@reventlessdev/online-shop-hybrid-seed 1.0.0-alpha.2
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/CHANGELOG.md +10 -0
- package/LICENSE +202 -0
- package/lib/bs/.compiler.log +2 -0
- package/lib/bs/OnlineShopHybridSeed.cmi +0 -0
- package/lib/bs/OnlineShopHybridSeed.cmj +0 -0
- package/lib/bs/OnlineShopHybridSeed.cmt +0 -0
- package/lib/bs/OnlineShopHybridSeed.mlmap +4 -0
- package/lib/bs/build.ninja +0 -0
- package/lib/bs/compiler-info.json +8 -0
- package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmi +0 -0
- package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmj +0 -0
- package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmt +0 -0
- package/lib/bs/src/DemoCommands.ast +0 -0
- package/lib/bs/src/DemoCommands.res +154 -0
- package/lib/bs/src/DemoCommands.res.mjs +388 -0
- package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmi +0 -0
- package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmj +0 -0
- package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmt +0 -0
- package/lib/bs/src/DemoData.ast +0 -0
- package/lib/bs/src/DemoData.res +307 -0
- package/lib/bs/src/DemoData.res.mjs +481 -0
- package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmi +0 -0
- package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmj +0 -0
- package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmt +0 -0
- package/lib/bs/src/HybridSeedData.ast +0 -0
- package/lib/bs/src/HybridSeedData.res +418 -0
- package/lib/bs/src/HybridSeedData.res.mjs +317 -0
- package/lib/ocaml/.compiler.log +2 -0
- package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmi +0 -0
- package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmj +0 -0
- package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmt +0 -0
- package/lib/ocaml/DemoCommands.ast +0 -0
- package/lib/ocaml/DemoCommands.res +154 -0
- package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmi +0 -0
- package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmj +0 -0
- package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmt +0 -0
- package/lib/ocaml/DemoData.ast +0 -0
- package/lib/ocaml/DemoData.res +307 -0
- package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmi +0 -0
- package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmj +0 -0
- package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmt +0 -0
- package/lib/ocaml/HybridSeedData.ast +0 -0
- package/lib/ocaml/HybridSeedData.res +418 -0
- package/lib/ocaml/OnlineShopHybridSeed.cmi +0 -0
- package/lib/ocaml/OnlineShopHybridSeed.cmj +0 -0
- package/lib/ocaml/OnlineShopHybridSeed.cmt +0 -0
- package/lib/ocaml/OnlineShopHybridSeed.mlmap +4 -0
- package/package.json +34 -0
- package/rescript.json +27 -0
- package/src/DemoCommands.res +154 -0
- package/src/DemoCommands.res.mjs +388 -0
- package/src/DemoData.res +307 -0
- package/src/DemoData.res.mjs +481 -0
- package/src/HybridSeedData.res +418 -0
- package/src/HybridSeedData.res.mjs +317 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// The online-shop-hybrid demo data set(s), exported as `Seed.dataSet` values.
|
|
2
|
+
//
|
|
3
|
+
// This is the shareable artifact: `dataSets` carries everything domain-specific
|
|
4
|
+
// — the phases, the view verification, and the summary board — behind the
|
|
5
|
+
// generic `Seed.dataSet` shape, so any platform script (this example's
|
|
6
|
+
// `platform-local`/`platform-aws`, or a downstream consumer) seeds it with just
|
|
7
|
+
// `Seed.Runner.seed(~sets=HybridSeedData.dataSets, ~connect=…)`.
|
|
8
|
+
//
|
|
9
|
+
// Because every phase drives the public GraphQL command API, a run doubles as a
|
|
10
|
+
// smoke test: it exercises the DCB append path, both extension points, and the
|
|
11
|
+
// AutoShipOrder automation. `DemoData.res` holds *what* to seed; `DemoCommands.res`
|
|
12
|
+
// maps command values onto mutations; the transport and reporting come from
|
|
13
|
+
// `ReventlessSeed`.
|
|
14
|
+
|
|
15
|
+
open ReventlessSeed
|
|
16
|
+
|
|
17
|
+
// Every queryable view this example exposes. `Unfillable` is not a way to
|
|
18
|
+
// silence a gap — it records a view no volume of seed data can reach, with the
|
|
19
|
+
// reason, so a zero is never read as a seeding miss.
|
|
20
|
+
let views = [
|
|
21
|
+
Seed.Runner.Seeded("Catalog_Categories"),
|
|
22
|
+
Seed.Runner.Seeded("Catalog_Products"),
|
|
23
|
+
Seed.Runner.Seeded("Catalog_ProductDemands"),
|
|
24
|
+
Seed.Runner.Seeded("Catalog_ImportProductAudits"),
|
|
25
|
+
Seed.Runner.Seeded("Ordering_AvailableProducts"),
|
|
26
|
+
Seed.Runner.Seeded("Ordering_Orders"),
|
|
27
|
+
Seed.Runner.Seeded("Ordering_Customers"),
|
|
28
|
+
Seed.Runner.Seeded("Ordering_AutoShipOrderTodos"),
|
|
29
|
+
Seed.Runner.Seeded("Ordering_SendOrderConfirmationTodos"),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
// ── Phases ──────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
let seedCategories = async (~client: Seed.Client.t) => {
|
|
35
|
+
await client->Seed.Client.sendAll(
|
|
36
|
+
DemoData.categories->Array.map(c =>
|
|
37
|
+
DemoCommands.addCategory(AddCategory({categoryId: c.id, name: c.name}))
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
Seed.Runner.report(`categories: ${(DemoData.categories->Array.length)->Int.toString} added`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Upload each product's deterministic placeholder SVG through the deployment's
|
|
44
|
+
// upload endpoint and swap `imageUrl` for the returned served `/{prefix}/{key}`
|
|
45
|
+
// ref, so the demo image travels the real upload → store → serve loop (local
|
|
46
|
+
// dev store or AWS bucket) exactly like a user upload — no external image URL.
|
|
47
|
+
let uploadProductImages = async (
|
|
48
|
+
products: array<DemoData.product>,
|
|
49
|
+
~client: Seed.Client.t,
|
|
50
|
+
~uploadEndpoint: string,
|
|
51
|
+
): array<DemoData.product> => {
|
|
52
|
+
let out = []
|
|
53
|
+
for i in 0 to products->Array.length - 1 {
|
|
54
|
+
switch products->Array.get(i) {
|
|
55
|
+
| Some(p) =>
|
|
56
|
+
let svg = DemoData.productSvg(~name=p.name, ~index=i)
|
|
57
|
+
switch await Seed.Upload.uploadAsset(
|
|
58
|
+
~uploadEndpoint,
|
|
59
|
+
~bytes=svg,
|
|
60
|
+
~fileName=`${p.id}.svg`,
|
|
61
|
+
~contentType="image/svg+xml",
|
|
62
|
+
~authToken=?Seed.Client.currentToken(client),
|
|
63
|
+
) {
|
|
64
|
+
| Ok(servedRef) => out->Array.push({...p, imageUrl: servedRef})
|
|
65
|
+
| Error(msg) => throw(Seed.Failed(`product image upload for ${p.id} failed: ${msg}`))
|
|
66
|
+
}
|
|
67
|
+
| None => ()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
Seed.Runner.report(
|
|
71
|
+
`product images: ${(out->Array.length)->Int.toString} uploaded to the served bucket`,
|
|
72
|
+
)
|
|
73
|
+
out
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let seedProducts = async (products: array<DemoData.product>, ~client: Seed.Client.t) => {
|
|
77
|
+
await client->Seed.Client.sendAll(
|
|
78
|
+
products->Array.map(p =>
|
|
79
|
+
DemoCommands.addProduct(
|
|
80
|
+
AddProduct({
|
|
81
|
+
productId: p.id,
|
|
82
|
+
name: p.name,
|
|
83
|
+
description: p.description,
|
|
84
|
+
price: p.price,
|
|
85
|
+
imageUrl: p.imageUrl,
|
|
86
|
+
categoryId: p.categoryId,
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
Seed.Runner.report(`products: ${(products->Array.length)->Int.toString} added`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let seedCatalogEdits = async (products: array<DemoData.product>, ~client: Seed.Client.t) => {
|
|
95
|
+
let repriced = DemoData.repricedProducts(products)
|
|
96
|
+
await client->Seed.Client.sendAll(
|
|
97
|
+
repriced->Array.map(p =>
|
|
98
|
+
DemoCommands.changeProductPrice(
|
|
99
|
+
ChangeProductPrice({productId: p.id, price: DemoData.discountedPrice(p)}),
|
|
100
|
+
)
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
let redescribed = DemoData.redescribedProducts(products)
|
|
104
|
+
await client->Seed.Client.sendAll(
|
|
105
|
+
redescribed->Array.map(p =>
|
|
106
|
+
DemoCommands.changeProductDescription(
|
|
107
|
+
ChangeProductDescription({
|
|
108
|
+
productId: p.id,
|
|
109
|
+
description: `${p.description} Updated listing copy.`,
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
await client->Seed.Client.sendAll([
|
|
115
|
+
DemoCommands.renameCategory(
|
|
116
|
+
RenameCategory({
|
|
117
|
+
categoryId: DemoData.renamedCategoryId,
|
|
118
|
+
name: DemoData.renamedCategoryName,
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
])
|
|
122
|
+
let archived = DemoData.categories->Array.filter(c => c.archive)
|
|
123
|
+
await client->Seed.Client.sendAll(
|
|
124
|
+
archived->Array.map(c => DemoCommands.archiveCategory(ArchiveCategory({categoryId: c.id}))),
|
|
125
|
+
)
|
|
126
|
+
Seed.Runner.report(
|
|
127
|
+
`catalog edits: ${(repriced->Array.length)->Int.toString} repriced, ${(redescribed
|
|
128
|
+
->Array.length)
|
|
129
|
+
->Int.toString} redescribed, 1 renamed, ${(archived->Array.length)
|
|
130
|
+
->Int.toString} archived`,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let seedSupplierFeed = async (~client: Seed.Client.t) => {
|
|
135
|
+
for i in 0 to DemoData.supplierFeed->Array.length - 1 {
|
|
136
|
+
switch DemoData.supplierFeed->Array.get(i) {
|
|
137
|
+
| Some(row) =>
|
|
138
|
+
// One feed row is deliberately invalid, so a translation rejection is an
|
|
139
|
+
// expected outcome here rather than a seeding failure.
|
|
140
|
+
(
|
|
141
|
+
await client->Seed.Client.send(
|
|
142
|
+
DemoCommands.importProduct(row),
|
|
143
|
+
~tolerate=["TranslationFailed"],
|
|
144
|
+
)
|
|
145
|
+
)->ignore
|
|
146
|
+
| None => ()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Cross-check the per-request outcomes against the slice's audit view.
|
|
150
|
+
let audits = await client->Seed.Client.queryAllNodes(
|
|
151
|
+
~field="Catalog_ImportProductAudits",
|
|
152
|
+
~selection="status",
|
|
153
|
+
)
|
|
154
|
+
let countWith = status =>
|
|
155
|
+
audits->Array.filter(a => a->Seed.Client.nodeString("status") == Some(status))->Array.length
|
|
156
|
+
let successes = countWith("Success")
|
|
157
|
+
let failures = countWith("Failure")
|
|
158
|
+
if (
|
|
159
|
+
successes != DemoData.expectedImportSuccesses || failures != DemoData.expectedImportFailures
|
|
160
|
+
) {
|
|
161
|
+
throw(
|
|
162
|
+
Seed.Failed(
|
|
163
|
+
`supplier feed: expected ${DemoData.expectedImportSuccesses->Int.toString} Success / ${DemoData.expectedImportFailures->Int.toString} Failure audit rows, got ${successes->Int.toString}/${failures->Int.toString}`,
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
Seed.Runner.report(
|
|
168
|
+
`supplier feed: ${successes->Int.toString} imported, ${failures->Int.toString} rejected (both shown in the audit view)`,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let seedCustomers = async (customers: array<DemoData.customer>, ~client: Seed.Client.t) => {
|
|
173
|
+
await client->Seed.Client.sendAll(
|
|
174
|
+
customers->Array.map(c =>
|
|
175
|
+
DemoCommands.customer(~id=c.id, Register({email: c.email, address: c.address}))
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
// Give every customer a map coordinate so the Customers map view renders a
|
|
179
|
+
// pin per row. The geo-point payload is a `{lat, lng}` input object.
|
|
180
|
+
await client->Seed.Client.sendAll(
|
|
181
|
+
customers->Array.map(c =>
|
|
182
|
+
DemoCommands.customer(~id=c.id, SetLocation({location: {lat: c.lat, lng: c.lng}}))
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
let moved = DemoData.movedCustomers(customers)
|
|
186
|
+
await client->Seed.Client.sendAll(
|
|
187
|
+
moved->Array.map(c =>
|
|
188
|
+
DemoCommands.customer(~id=c.id, UpdateAddress({address: DemoData.newAddress()}))
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
Seed.Runner.report(
|
|
192
|
+
`customers: ${(customers->Array.length)->Int.toString} registered, ${(moved->Array.length)
|
|
193
|
+
->Int.toString} moved`,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let seedOrders = async (orders: array<DemoData.order>, ~client: Seed.Client.t) => {
|
|
198
|
+
for i in 0 to orders->Array.length - 1 {
|
|
199
|
+
switch orders->Array.get(i) {
|
|
200
|
+
| Some(order) =>
|
|
201
|
+
(await client->Seed.Client.send(
|
|
202
|
+
DemoCommands.placeOrder(
|
|
203
|
+
PlaceOrder({
|
|
204
|
+
orderId: order.id,
|
|
205
|
+
customerId: order.customerId,
|
|
206
|
+
productIds: order.productIds,
|
|
207
|
+
shippingMethod: order.shippingMethod,
|
|
208
|
+
}),
|
|
209
|
+
),
|
|
210
|
+
))->ignore
|
|
211
|
+
if mod(i + 1, 50) == 0 {
|
|
212
|
+
Seed.Runner.report(
|
|
213
|
+
`orders: ${(i + 1)->Int.toString}/${(orders->Array.length)->Int.toString} placed`,
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
| None => ()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
let countMethod = (method: OrderingPlugin.PlaceOrder.shippingMethod) =>
|
|
220
|
+
orders->Array.filter(o => o.shippingMethod == method)->Array.length
|
|
221
|
+
Seed.Runner.report(
|
|
222
|
+
`orders: ${(orders->Array.length)->Int.toString} placed (Standard ${countMethod(
|
|
223
|
+
Standard,
|
|
224
|
+
)->Int.toString}, Express ${countMethod(Express)->Int.toString}, Pickup ${countMethod(
|
|
225
|
+
Pickup,
|
|
226
|
+
)->Int.toString}) — Express placements were auto-shipped on arrival`,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let dispatchStandardBatch = async (orders: array<DemoData.order>, ~client: Seed.Client.t) => {
|
|
231
|
+
let dispatched = DemoData.batchDispatched(orders)
|
|
232
|
+
await client->Seed.Client.sendAll(
|
|
233
|
+
dispatched->Array.map(o => DemoCommands.shipOrder(ShipOrder({orderId: o.id}))),
|
|
234
|
+
)
|
|
235
|
+
let standardTotal = orders->Array.filter(o => o.shippingMethod == Standard)->Array.length
|
|
236
|
+
Seed.Runner.report(
|
|
237
|
+
`batch dispatch: ${(dispatched->Array.length)
|
|
238
|
+
->Int.toString}/${standardTotal->Int.toString} Standard orders shipped, ${(standardTotal -
|
|
239
|
+
dispatched->Array.length)->Int.toString} left pending`,
|
|
240
|
+
)
|
|
241
|
+
dispatched->Array.map(o => o.id)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let seedCancellations = async (
|
|
245
|
+
orders: array<DemoData.order>,
|
|
246
|
+
~client: Seed.Client.t,
|
|
247
|
+
~dispatched: array<string>,
|
|
248
|
+
) => {
|
|
249
|
+
let cancellable = DemoData.cancellable(orders, ~dispatched)
|
|
250
|
+
let targets = DemoData.cancelled(cancellable)
|
|
251
|
+
await client->Seed.Client.sendAll(
|
|
252
|
+
targets->Array.map(o => DemoCommands.cancelOrder(CancelOrder({orderId: o.id}))),
|
|
253
|
+
)
|
|
254
|
+
Seed.Runner.report(
|
|
255
|
+
`cancellations: ${(targets->Array.length)->Int.toString} of ${(cancellable->Array.length)
|
|
256
|
+
->Int.toString} cancellable orders cancelled`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let seedDeactivations = async (customers: array<DemoData.customer>, ~client: Seed.Client.t) => {
|
|
261
|
+
let targets = DemoData.deactivatedCustomers(customers)
|
|
262
|
+
await client->Seed.Client.sendAll(
|
|
263
|
+
targets->Array.map(c => DemoCommands.customer(~id=c.id, Deactivate)),
|
|
264
|
+
)
|
|
265
|
+
Seed.Runner.report(`customers: ${(targets->Array.length)->Int.toString} deactivated`)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
let summarise = async (~client: Seed.Client.t, ~counts: dict<int>) => {
|
|
271
|
+
let orders = await client->Seed.Client.queryAllNodes(
|
|
272
|
+
~field="Ordering_Orders",
|
|
273
|
+
~selection="status shippingMethod",
|
|
274
|
+
)
|
|
275
|
+
let statuses = ["Placed", "Shipped", "Cancelled"]
|
|
276
|
+
let methods = ["Standard", "Express", "Pickup"]
|
|
277
|
+
let countCell = (status, method) =>
|
|
278
|
+
orders
|
|
279
|
+
->Array.filter(o =>
|
|
280
|
+
o->Seed.Client.nodeString("status") == Some(status) &&
|
|
281
|
+
o->Seed.Client.nodeString("shippingMethod") == Some(method)
|
|
282
|
+
)
|
|
283
|
+
->Array.length
|
|
284
|
+
let countStatus = status =>
|
|
285
|
+
orders->Array.filter(o => o->Seed.Client.nodeString("status") == Some(status))->Array.length
|
|
286
|
+
|
|
287
|
+
let demand = await client->Seed.Client.queryAllNodes(
|
|
288
|
+
~field="Catalog_ProductDemands",
|
|
289
|
+
~selection="name orderCount",
|
|
290
|
+
)
|
|
291
|
+
let orderCountOf = node =>
|
|
292
|
+
switch node->Seed.Client.field("orderCount") {
|
|
293
|
+
| Some(Number(n)) => n->Int.fromFloat
|
|
294
|
+
| _ => 0
|
|
295
|
+
}
|
|
296
|
+
let ranked = demand->Array.toSorted((a, b) => Int.compare(orderCountOf(b), orderCountOf(a)))
|
|
297
|
+
let withDemand = ranked->Array.filter(n => orderCountOf(n) > 0)
|
|
298
|
+
let head =
|
|
299
|
+
ranked
|
|
300
|
+
->Array.slice(~start=0, ~end=3)
|
|
301
|
+
->Array.map(n =>
|
|
302
|
+
`${n->Seed.Client.nodeString("name")->Option.getOr("?")} (${orderCountOf(n)->Int.toString})`
|
|
303
|
+
)
|
|
304
|
+
->Array.join(", ")
|
|
305
|
+
|
|
306
|
+
Seed.Runner.heading("Seeded:")
|
|
307
|
+
Console.log(
|
|
308
|
+
` order status: ${statuses
|
|
309
|
+
->Array.map(s => `${s} ${countStatus(s)->Int.toString}`)
|
|
310
|
+
->Array.join(", ")}`,
|
|
311
|
+
)
|
|
312
|
+
Console.log(` demand head: ${head}`)
|
|
313
|
+
Console.log(
|
|
314
|
+
` demand spread: ${(withDemand->Array.length)->Int.toString}/${(ranked->Array.length)
|
|
315
|
+
->Int.toString} products ordered, tail min ${withDemand
|
|
316
|
+
->Array.last
|
|
317
|
+
->Option.mapOr(0, orderCountOf)
|
|
318
|
+
->Int.toString}`,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
Console.log("")
|
|
322
|
+
Console.log(" status × shippingMethod:")
|
|
323
|
+
Console.log(
|
|
324
|
+
` ${" "->String.padEnd(10, " ")}${methods
|
|
325
|
+
->Array.map(m => m->String.padStart(10, " "))
|
|
326
|
+
->Array.join("")}`,
|
|
327
|
+
)
|
|
328
|
+
statuses->Array.forEach(status =>
|
|
329
|
+
Console.log(
|
|
330
|
+
` ${status->String.padEnd(10, " ")}${methods
|
|
331
|
+
->Array.map(m => countCell(status, m)->Int.toString->String.padStart(10, " "))
|
|
332
|
+
->Array.join("")}`,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
let observedStatuses = statuses->Array.filter(s => countStatus(s) > 0)
|
|
337
|
+
let spreadWarning = if observedStatuses->Array.length < 3 {
|
|
338
|
+
[
|
|
339
|
+
`the board shows only ${(observedStatuses->Array.length)
|
|
340
|
+
->Int.toString} of the 3 order statuses (${observedStatuses->Array.join(
|
|
341
|
+
", ",
|
|
342
|
+
)}) — the shipping-method mix or the batch-dispatch share needs adjusting.`,
|
|
343
|
+
]
|
|
344
|
+
} else {
|
|
345
|
+
[]
|
|
346
|
+
}
|
|
347
|
+
Seed.Runner.warn(Array.concat(spreadWarning, Seed.Runner.unfillableWarnings(~views, ~counts)))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── Run ─────────────────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
// The shared seed run, parameterized only by volume so every data set walks the
|
|
353
|
+
// same phases. Uploads no-op when the connection carries no upload endpoint
|
|
354
|
+
// (a deployment that serves none), keeping `imageUrl` empty rather than failing.
|
|
355
|
+
let run = async (
|
|
356
|
+
connection: Seed.connection,
|
|
357
|
+
~productCount: int,
|
|
358
|
+
~customerCount: int,
|
|
359
|
+
~orderCount: int,
|
|
360
|
+
): unit => {
|
|
361
|
+
let client = connection.client
|
|
362
|
+
|
|
363
|
+
let built = DemoData.buildProducts(~count=productCount, ())
|
|
364
|
+
let products = if connection.uploadEndpoint == "" {
|
|
365
|
+
Seed.Runner.report(
|
|
366
|
+
"product images: skipped (no upload endpoint / SEED_SKIP_UPLOADS) — imageUrl left empty",
|
|
367
|
+
)
|
|
368
|
+
built
|
|
369
|
+
} else {
|
|
370
|
+
await uploadProductImages(built, ~client, ~uploadEndpoint=connection.uploadEndpoint)
|
|
371
|
+
}
|
|
372
|
+
let customers = DemoData.buildCustomers(~count=customerCount, ())
|
|
373
|
+
let orders = DemoData.buildOrders(products, customers, ~count=orderCount, ())
|
|
374
|
+
|
|
375
|
+
await seedCategories(~client)
|
|
376
|
+
await seedProducts(products, ~client)
|
|
377
|
+
await seedCatalogEdits(products, ~client)
|
|
378
|
+
await seedSupplierFeed(~client)
|
|
379
|
+
await seedCustomers(customers, ~client)
|
|
380
|
+
|
|
381
|
+
// Products reach Ordering asynchronously through the Products extension
|
|
382
|
+
// point; PlaceOrder rejects until the shadow copy lands.
|
|
383
|
+
let expected = Array.concat(products->Array.map(p => p.id), DemoData.importedSkus)
|
|
384
|
+
let available = await client->Seed.Client.waitForIds(
|
|
385
|
+
~field="Ordering_AvailableProductsByIds",
|
|
386
|
+
~ids=expected,
|
|
387
|
+
)
|
|
388
|
+
Seed.Runner.report(`ordering: ${available->Int.toString} products available`)
|
|
389
|
+
|
|
390
|
+
await seedOrders(orders, ~client)
|
|
391
|
+
let dispatched = await dispatchStandardBatch(orders, ~client)
|
|
392
|
+
await seedCancellations(orders, ~client, ~dispatched)
|
|
393
|
+
await seedDeactivations(customers, ~client)
|
|
394
|
+
|
|
395
|
+
let counts = await Seed.Runner.verifyViews(client, ~views)
|
|
396
|
+
await summarise(~client, ~counts)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Two sets, so `Seed.Runner.seed` presents a selection: the full demo, and a
|
|
400
|
+
// compact sample for a quick end-to-end check. Both walk identical phases.
|
|
401
|
+
let dataSets: array<Seed.dataSet> = [
|
|
402
|
+
{
|
|
403
|
+
name: "full",
|
|
404
|
+
label: `full — ${DemoData.productCount->Int.toString} products, ${DemoData.customerCount->Int.toString} customers, ${DemoData.orderCount->Int.toString} orders`,
|
|
405
|
+
seed: connection =>
|
|
406
|
+
run(
|
|
407
|
+
connection,
|
|
408
|
+
~productCount=DemoData.productCount,
|
|
409
|
+
~customerCount=DemoData.customerCount,
|
|
410
|
+
~orderCount=DemoData.orderCount,
|
|
411
|
+
),
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "sample",
|
|
415
|
+
label: "sample — 16 products, 8 customers, 40 orders",
|
|
416
|
+
seed: connection => run(connection, ~productCount=16, ~customerCount=8, ~orderCount=40),
|
|
417
|
+
},
|
|
418
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reventlessdev/online-shop-hybrid-seed",
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
|
+
"description": "Shareable demo data set(s) for the online-shop-hybrid example, exported as reventless-seed data sets",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"sury": "^11.0.0-alpha.4",
|
|
8
|
+
"@reventlessdev/online-shop-hybrid-ordering": "1.0.0-alpha.155",
|
|
9
|
+
"@reventlessdev/online-shop-hybrid-catalog-spec": "1.0.0-alpha.74",
|
|
10
|
+
"@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.74",
|
|
11
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.2",
|
|
12
|
+
"@reventlessdev/online-shop-hybrid-catalog": "1.0.0-alpha.156"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"rescript": "12.3.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"rescript": "12.3.0"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"registry": "https://registry.npmjs.org"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/ReventlessDev/reventless-core.git",
|
|
26
|
+
"directory": "examples/online-shop-hybrid/seed-data"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "rescript build",
|
|
30
|
+
"start": "rescript watch",
|
|
31
|
+
"clean": "rescript clean",
|
|
32
|
+
"rebuild": "pnpm run clean && pnpm run build"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/rescript.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reventlessdev/online-shop-hybrid-seed",
|
|
3
|
+
"package-specs": {
|
|
4
|
+
"module": "esmodule",
|
|
5
|
+
"in-source": true
|
|
6
|
+
},
|
|
7
|
+
"namespace": "OnlineShopHybridSeed",
|
|
8
|
+
"warnings": {
|
|
9
|
+
"error": "-44+101"
|
|
10
|
+
},
|
|
11
|
+
"sources": [
|
|
12
|
+
{
|
|
13
|
+
"dir": "src",
|
|
14
|
+
"subdirs": true
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"dependencies": [
|
|
18
|
+
"sury",
|
|
19
|
+
"@reventlessdev/reventless-seed",
|
|
20
|
+
"@reventlessdev/online-shop-hybrid-catalog",
|
|
21
|
+
"@reventlessdev/online-shop-hybrid-catalog-spec",
|
|
22
|
+
"@reventlessdev/online-shop-hybrid-ordering",
|
|
23
|
+
"@reventlessdev/online-shop-hybrid-ordering-spec"
|
|
24
|
+
],
|
|
25
|
+
"compiler-flags": [],
|
|
26
|
+
"suffix": ".res.mjs"
|
|
27
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Adapter: online-shop-hybrid command values → GraphQL mutations.
|
|
2
|
+
//
|
|
3
|
+
// This is the only place that knows how this example's commands are exposed on
|
|
4
|
+
// the API — the mutation field names and how each variant's payload maps onto
|
|
5
|
+
// arguments. Everything above it works in plugin command types; everything
|
|
6
|
+
// below it (`ReventlessSeed`) works in generic mutations.
|
|
7
|
+
//
|
|
8
|
+
// Each encoder pattern-matches the real command variant, so a renamed field or
|
|
9
|
+
// a new payload member is a compile error here rather than a rejected mutation
|
|
10
|
+
// at seed time. That is the whole point of routing the seed through typed
|
|
11
|
+
// commands instead of hand-written GraphQL strings.
|
|
12
|
+
|
|
13
|
+
open ReventlessSeed
|
|
14
|
+
|
|
15
|
+
// Mutation fields are `<PluginName>_<CommandVariant>`; aggregates additionally
|
|
16
|
+
// carry the entity id as a separate `id` argument.
|
|
17
|
+
let catalog = (name: string): string => `Catalog_${name}`
|
|
18
|
+
let ordering = (name: string): string => `Ordering_${name}`
|
|
19
|
+
|
|
20
|
+
// ── Catalog ─────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
let addCategory = (command: CatalogPlugin.AddCategory.command): Seed.mutation =>
|
|
23
|
+
switch command {
|
|
24
|
+
| AddCategory({categoryId, name}) =>
|
|
25
|
+
Seed.mutation(catalog("AddCategory"), [("categoryId", Id(categoryId)), ("name", String(name))])
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let renameCategory = (command: CatalogPlugin.RenameCategory.command): Seed.mutation =>
|
|
29
|
+
switch command {
|
|
30
|
+
| RenameCategory({categoryId, name}) =>
|
|
31
|
+
Seed.mutation(
|
|
32
|
+
catalog("RenameCategory"),
|
|
33
|
+
[("categoryId", Id(categoryId)), ("name", String(name))],
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let archiveCategory = (command: CatalogPlugin.ArchiveCategory.command): Seed.mutation =>
|
|
38
|
+
switch command {
|
|
39
|
+
| ArchiveCategory({categoryId}) =>
|
|
40
|
+
Seed.mutation(catalog("ArchiveCategory"), [("categoryId", Id(categoryId))])
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let addProduct = (command: CatalogPlugin.AddProduct.command): Seed.mutation =>
|
|
44
|
+
switch command {
|
|
45
|
+
| AddProduct({productId, name, description, price, imageUrl, categoryId}) =>
|
|
46
|
+
Seed.mutation(
|
|
47
|
+
catalog("AddProduct"),
|
|
48
|
+
[
|
|
49
|
+
("productId", Id(productId)),
|
|
50
|
+
("name", String(name)),
|
|
51
|
+
("description", String(description)),
|
|
52
|
+
("price", Float(price)),
|
|
53
|
+
("imageUrl", String(imageUrl)),
|
|
54
|
+
("categoryId", Id(categoryId)),
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let changeProductPrice = (command: CatalogPlugin.ChangeProductPrice.command): Seed.mutation =>
|
|
60
|
+
switch command {
|
|
61
|
+
| ChangeProductPrice({productId, price}) =>
|
|
62
|
+
Seed.mutation(
|
|
63
|
+
catalog("ChangeProductPrice"),
|
|
64
|
+
[("productId", Id(productId)), ("price", Float(price))],
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let changeProductDescription = (
|
|
69
|
+
command: CatalogPlugin.ChangeProductDescription.command,
|
|
70
|
+
): Seed.mutation =>
|
|
71
|
+
switch command {
|
|
72
|
+
| ChangeProductDescription({productId, description}) =>
|
|
73
|
+
Seed.mutation(
|
|
74
|
+
catalog("ChangeProductDescription"),
|
|
75
|
+
[("productId", Id(productId)), ("description", String(description))],
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** InboundTranslationSlice: the mutation arguments are the external input's
|
|
80
|
+
fields, not a command payload — the slice translates them itself. */
|
|
81
|
+
let importProduct = (input: CatalogPlugin.ImportProduct.externalInput): Seed.mutation =>
|
|
82
|
+
Seed.mutation(
|
|
83
|
+
catalog("ImportProduct"),
|
|
84
|
+
[
|
|
85
|
+
("sku", String(input.sku)),
|
|
86
|
+
("title", String(input.title)),
|
|
87
|
+
("desc", String(input.desc)),
|
|
88
|
+
("unitPrice", Int(input.unitPrice)),
|
|
89
|
+
("currency", String(input.currency)),
|
|
90
|
+
("category", Id(input.category)),
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
// ── Ordering ────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
let shippingMethod = (method: OrderingPlugin.PlaceOrder.shippingMethod): Seed.value =>
|
|
97
|
+
switch method {
|
|
98
|
+
| Standard => Enum("Standard")
|
|
99
|
+
| Express => Enum("Express")
|
|
100
|
+
| Pickup => Enum("Pickup")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let placeOrder = (command: OrderingPlugin.PlaceOrder.command): Seed.mutation =>
|
|
104
|
+
switch command {
|
|
105
|
+
| PlaceOrder({orderId, customerId, productIds, shippingMethod: method}) =>
|
|
106
|
+
Seed.mutation(
|
|
107
|
+
ordering("PlaceOrder"),
|
|
108
|
+
[
|
|
109
|
+
("orderId", Id(orderId)),
|
|
110
|
+
("customerId", Id(customerId)),
|
|
111
|
+
("productIds", Seed.ids(productIds)),
|
|
112
|
+
("shippingMethod", shippingMethod(method)),
|
|
113
|
+
],
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let shipOrder = (command: OrderingPlugin.ShipOrder.command): Seed.mutation =>
|
|
118
|
+
switch command {
|
|
119
|
+
| ShipOrder({orderId}) => Seed.mutation(ordering("ShipOrder"), [("orderId", Id(orderId))])
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** CancelOrder's ReopenOrder variant is `@noApi`, so it has no mutation field to
|
|
123
|
+
encode to — reaching it here means the seed tried to drive an internal-only
|
|
124
|
+
command through the public API. */
|
|
125
|
+
let cancelOrder = (command: OrderingPlugin.CancelOrder.command): Seed.mutation =>
|
|
126
|
+
switch command {
|
|
127
|
+
| CancelOrder({orderId}) => Seed.mutation(ordering("CancelOrder"), [("orderId", Id(orderId))])
|
|
128
|
+
| ReopenOrder(_) =>
|
|
129
|
+
throw(Seed.Failed("ReopenOrder is @noApi — it cannot be seeded through the GraphQL API"))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Aggregate commands are exposed as `<Plugin>_<Aggregate>_<Variant>` and take
|
|
133
|
+
the aggregate id alongside the payload. */
|
|
134
|
+
let customer = (~id: string, command: OrderingPlugin.Customer.command): Seed.mutation =>
|
|
135
|
+
switch command {
|
|
136
|
+
| Register({email, address}) =>
|
|
137
|
+
Seed.mutation(
|
|
138
|
+
ordering("Customer_Register"),
|
|
139
|
+
[("id", Id(id)), ("email", String(email)), ("address", String(address))],
|
|
140
|
+
)
|
|
141
|
+
| UpdateEmail({email}) =>
|
|
142
|
+
Seed.mutation(ordering("Customer_UpdateEmail"), [("id", Id(id)), ("email", String(email))])
|
|
143
|
+
| UpdateAddress({address}) =>
|
|
144
|
+
Seed.mutation(
|
|
145
|
+
ordering("Customer_UpdateAddress"),
|
|
146
|
+
[("id", Id(id)), ("address", String(address))],
|
|
147
|
+
)
|
|
148
|
+
| SetLocation({location}) =>
|
|
149
|
+
Seed.mutation(
|
|
150
|
+
ordering("Customer_SetLocation"),
|
|
151
|
+
[("id", Id(id)), ("location", Object([("lat", Float(location.lat)), ("lng", Float(location.lng))]))],
|
|
152
|
+
)
|
|
153
|
+
| Deactivate => Seed.mutation(ordering("Customer_Deactivate"), [("id", Id(id))])
|
|
154
|
+
}
|