@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +202 -0
  3. package/lib/bs/.compiler.log +2 -0
  4. package/lib/bs/OnlineShopHybridSeed.cmi +0 -0
  5. package/lib/bs/OnlineShopHybridSeed.cmj +0 -0
  6. package/lib/bs/OnlineShopHybridSeed.cmt +0 -0
  7. package/lib/bs/OnlineShopHybridSeed.mlmap +4 -0
  8. package/lib/bs/build.ninja +0 -0
  9. package/lib/bs/compiler-info.json +8 -0
  10. package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmi +0 -0
  11. package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmj +0 -0
  12. package/lib/bs/src/DemoCommands-OnlineShopHybridSeed.cmt +0 -0
  13. package/lib/bs/src/DemoCommands.ast +0 -0
  14. package/lib/bs/src/DemoCommands.res +154 -0
  15. package/lib/bs/src/DemoCommands.res.mjs +388 -0
  16. package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmi +0 -0
  17. package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmj +0 -0
  18. package/lib/bs/src/DemoData-OnlineShopHybridSeed.cmt +0 -0
  19. package/lib/bs/src/DemoData.ast +0 -0
  20. package/lib/bs/src/DemoData.res +307 -0
  21. package/lib/bs/src/DemoData.res.mjs +481 -0
  22. package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmi +0 -0
  23. package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmj +0 -0
  24. package/lib/bs/src/HybridSeedData-OnlineShopHybridSeed.cmt +0 -0
  25. package/lib/bs/src/HybridSeedData.ast +0 -0
  26. package/lib/bs/src/HybridSeedData.res +418 -0
  27. package/lib/bs/src/HybridSeedData.res.mjs +317 -0
  28. package/lib/ocaml/.compiler.log +2 -0
  29. package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmi +0 -0
  30. package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmj +0 -0
  31. package/lib/ocaml/DemoCommands-OnlineShopHybridSeed.cmt +0 -0
  32. package/lib/ocaml/DemoCommands.ast +0 -0
  33. package/lib/ocaml/DemoCommands.res +154 -0
  34. package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmi +0 -0
  35. package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmj +0 -0
  36. package/lib/ocaml/DemoData-OnlineShopHybridSeed.cmt +0 -0
  37. package/lib/ocaml/DemoData.ast +0 -0
  38. package/lib/ocaml/DemoData.res +307 -0
  39. package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmi +0 -0
  40. package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmj +0 -0
  41. package/lib/ocaml/HybridSeedData-OnlineShopHybridSeed.cmt +0 -0
  42. package/lib/ocaml/HybridSeedData.ast +0 -0
  43. package/lib/ocaml/HybridSeedData.res +418 -0
  44. package/lib/ocaml/OnlineShopHybridSeed.cmi +0 -0
  45. package/lib/ocaml/OnlineShopHybridSeed.cmj +0 -0
  46. package/lib/ocaml/OnlineShopHybridSeed.cmt +0 -0
  47. package/lib/ocaml/OnlineShopHybridSeed.mlmap +4 -0
  48. package/package.json +34 -0
  49. package/rescript.json +27 -0
  50. package/src/DemoCommands.res +154 -0
  51. package/src/DemoCommands.res.mjs +388 -0
  52. package/src/DemoData.res +307 -0
  53. package/src/DemoData.res.mjs +481 -0
  54. package/src/HybridSeedData.res +418 -0
  55. 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
+ ]