@reventlessdev/online-shop-hybrid-seed 1.0.0-alpha.10

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 +72 -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 +443 -0
  27. package/lib/bs/src/HybridSeedData.res.mjs +335 -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 +443 -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 +443 -0
  55. package/src/HybridSeedData.res.mjs +335 -0
@@ -0,0 +1,443 @@
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
+ // The fresh-store guard probes these before sending any command — the `Seeded`
33
+ // view names, so the pre-flight refusal stays in lockstep with `verifyViews`.
34
+ // `Catalog_Categories` leads, so a populated store aborts after one query.
35
+ let probeViews =
36
+ views->Array.filterMap(v =>
37
+ switch v {
38
+ | Seed.Runner.Seeded(name) => Some(name)
39
+ | Seed.Runner.Unfillable(_, _) => None
40
+ }
41
+ )
42
+
43
+ // ── Phases ──────────────────────────────────────────────────────────────────
44
+
45
+ let seedCategories = async (~client: Seed.Client.t) => {
46
+ await client->Seed.Client.sendAll(
47
+ DemoData.categories->Array.map(c =>
48
+ DemoCommands.addCategory(AddCategory({categoryId: c.id, name: c.name}))
49
+ ),
50
+ )
51
+ Seed.Runner.report(`categories: ${(DemoData.categories->Array.length)->Int.toString} added`)
52
+ }
53
+
54
+ // Upload each product's deterministic placeholder SVG through the deployment's
55
+ // upload endpoint and swap `imageUrl` for the returned served `/{prefix}/{key}`
56
+ // ref, so the demo image travels the real upload → store → serve loop (local
57
+ // dev store or AWS bucket) exactly like a user upload — no external image URL.
58
+ let uploadProductImages = async (
59
+ products: array<DemoData.product>,
60
+ ~client: Seed.Client.t,
61
+ ~uploadEndpoint: string,
62
+ ): array<DemoData.product> => {
63
+ let out = []
64
+ for i in 0 to products->Array.length - 1 {
65
+ switch products->Array.get(i) {
66
+ | Some(p) =>
67
+ let svg = DemoData.productSvg(~name=p.name, ~index=i)
68
+ switch await Seed.Upload.uploadAsset(
69
+ ~uploadEndpoint,
70
+ ~bytes=svg,
71
+ ~fileName=`${p.id}.svg`,
72
+ ~contentType="image/svg+xml",
73
+ ~authToken=?Seed.Client.currentToken(client),
74
+ ) {
75
+ | Ok(servedRef) => out->Array.push({...p, imageUrl: servedRef})
76
+ | Error(msg) => throw(Seed.Failed(`product image upload for ${p.id} failed: ${msg}`))
77
+ }
78
+ | None => ()
79
+ }
80
+ }
81
+ Seed.Runner.report(
82
+ `product images: ${(out->Array.length)->Int.toString} uploaded to the served bucket`,
83
+ )
84
+ out
85
+ }
86
+
87
+ let seedProducts = async (products: array<DemoData.product>, ~client: Seed.Client.t) => {
88
+ await client->Seed.Client.sendAll(
89
+ products->Array.map(p =>
90
+ DemoCommands.addProduct(
91
+ AddProduct({
92
+ productId: p.id,
93
+ name: p.name,
94
+ description: p.description,
95
+ price: p.price,
96
+ imageUrl: p.imageUrl,
97
+ categoryId: p.categoryId,
98
+ }),
99
+ )
100
+ ),
101
+ )
102
+ Seed.Runner.report(`products: ${(products->Array.length)->Int.toString} added`)
103
+ }
104
+
105
+ let seedCatalogEdits = async (products: array<DemoData.product>, ~client: Seed.Client.t) => {
106
+ let repriced = DemoData.repricedProducts(products)
107
+ await client->Seed.Client.sendAll(
108
+ repriced->Array.map(p =>
109
+ DemoCommands.changeProductPrice(
110
+ ChangeProductPrice({productId: p.id, price: DemoData.discountedPrice(p)}),
111
+ )
112
+ ),
113
+ )
114
+ let redescribed = DemoData.redescribedProducts(products)
115
+ await client->Seed.Client.sendAll(
116
+ redescribed->Array.map(p =>
117
+ DemoCommands.changeProductDescription(
118
+ ChangeProductDescription({
119
+ productId: p.id,
120
+ description: `${p.description} Updated listing copy.`,
121
+ }),
122
+ )
123
+ ),
124
+ )
125
+ await client->Seed.Client.sendAll([
126
+ DemoCommands.renameCategory(
127
+ RenameCategory({
128
+ categoryId: DemoData.renamedCategoryId,
129
+ name: DemoData.renamedCategoryName,
130
+ }),
131
+ ),
132
+ ])
133
+ let archived = DemoData.categories->Array.filter(c => c.archive)
134
+ await client->Seed.Client.sendAll(
135
+ archived->Array.map(c => DemoCommands.archiveCategory(ArchiveCategory({categoryId: c.id}))),
136
+ )
137
+ Seed.Runner.report(
138
+ `catalog edits: ${(repriced->Array.length)->Int.toString} repriced, ${(redescribed
139
+ ->Array.length)
140
+ ->Int.toString} redescribed, 1 renamed, ${(archived->Array.length)
141
+ ->Int.toString} archived`,
142
+ )
143
+ }
144
+
145
+ let seedSupplierFeed = async (~client: Seed.Client.t) => {
146
+ for i in 0 to DemoData.supplierFeed->Array.length - 1 {
147
+ switch DemoData.supplierFeed->Array.get(i) {
148
+ | Some(row) =>
149
+ // One feed row is deliberately invalid, so a translation rejection is an
150
+ // expected outcome here rather than a seeding failure.
151
+ (
152
+ await client->Seed.Client.send(
153
+ DemoCommands.importProduct(row),
154
+ ~tolerate=["TranslationFailed"],
155
+ )
156
+ )->ignore
157
+ | None => ()
158
+ }
159
+ }
160
+ // Cross-check the per-request outcomes against the slice's audit view. The
161
+ // slice writes an audit row as it receives each feed row, but we read it back
162
+ // over a separate, eventually-consistent query — so poll until the expected
163
+ // tally appears rather than asserting on a single immediate read (which would
164
+ // race the projection and spuriously report 0/0). Same pattern as the id waits
165
+ // above.
166
+ let countWith = (audits, status) =>
167
+ audits->Array.filter(a => a->Seed.Client.nodeString("status") == Some(status))->Array.length
168
+ let expectedSuccesses = DemoData.expectedImportSuccesses
169
+ let expectedFailures = DemoData.expectedImportFailures
170
+ let expected = `${expectedSuccesses->Int.toString} Success / ${expectedFailures->Int.toString} Failure`
171
+ let audits = await client->Seed.Client.queryAllNodesUntil(
172
+ ~field="Catalog_ImportProductAudits",
173
+ ~selection="status",
174
+ ~satisfied=audits =>
175
+ audits->countWith("Success") == expectedSuccesses &&
176
+ audits->countWith("Failure") == expectedFailures,
177
+ ~onTimeout=audits => {
178
+ let successes = audits->countWith("Success")
179
+ let failures = audits->countWith("Failure")
180
+ let total = audits->Array.length
181
+ total == 0
182
+ ? `supplier feed: the import audit view (Catalog_ImportProductAudits) never became non-empty. All ${(DemoData.supplierFeed
183
+ ->Array.length)
184
+ ->Int.toString} import commands were accepted by the API, yet no audit rows appeared — the ImportProduct slice is not persisting its audit log (or it is not queryable through this view). Expected ${expected}.`
185
+ : `supplier feed: the audit view settled on ${successes->Int.toString} Success / ${failures->Int.toString} Failure (${total->Int.toString} rows), expected ${expected} — the feed produced a different set of outcomes than the seed data describes.`
186
+ },
187
+ )
188
+ let successes = audits->countWith("Success")
189
+ let failures = audits->countWith("Failure")
190
+ Seed.Runner.report(
191
+ `supplier feed: ${successes->Int.toString} imported, ${failures->Int.toString} rejected (both shown in the audit view)`,
192
+ )
193
+ }
194
+
195
+ let seedCustomers = async (customers: array<DemoData.customer>, ~client: Seed.Client.t) => {
196
+ await client->Seed.Client.sendAll(
197
+ customers->Array.map(c =>
198
+ DemoCommands.customer(~id=c.id, Register({email: c.email, address: c.address}))
199
+ ),
200
+ )
201
+ // Give every customer a map coordinate so the Customers map view renders a
202
+ // pin per row. The geo-point payload is a `{lat, lng}` input object.
203
+ await client->Seed.Client.sendAll(
204
+ customers->Array.map(c =>
205
+ DemoCommands.customer(~id=c.id, SetLocation({location: {lat: c.lat, lng: c.lng}}))
206
+ ),
207
+ )
208
+ let moved = DemoData.movedCustomers(customers)
209
+ await client->Seed.Client.sendAll(
210
+ moved->Array.map(c =>
211
+ DemoCommands.customer(~id=c.id, UpdateAddress({address: DemoData.newAddress()}))
212
+ ),
213
+ )
214
+ Seed.Runner.report(
215
+ `customers: ${(customers->Array.length)->Int.toString} registered, ${(moved->Array.length)
216
+ ->Int.toString} moved`,
217
+ )
218
+ }
219
+
220
+ let seedOrders = async (orders: array<DemoData.order>, ~client: Seed.Client.t) => {
221
+ for i in 0 to orders->Array.length - 1 {
222
+ switch orders->Array.get(i) {
223
+ | Some(order) =>
224
+ (await client->Seed.Client.send(
225
+ DemoCommands.placeOrder(
226
+ PlaceOrder({
227
+ orderId: order.id,
228
+ customerId: order.customerId,
229
+ productIds: order.productIds,
230
+ shippingMethod: order.shippingMethod,
231
+ }),
232
+ ),
233
+ ))->ignore
234
+ if mod(i + 1, 50) == 0 {
235
+ Seed.Runner.report(
236
+ `orders: ${(i + 1)->Int.toString}/${(orders->Array.length)->Int.toString} placed`,
237
+ )
238
+ }
239
+ | None => ()
240
+ }
241
+ }
242
+ let countMethod = (method: OrderingPlugin.PlaceOrder.shippingMethod) =>
243
+ orders->Array.filter(o => o.shippingMethod == method)->Array.length
244
+ Seed.Runner.report(
245
+ `orders: ${(orders->Array.length)->Int.toString} placed (Standard ${countMethod(
246
+ Standard,
247
+ )->Int.toString}, Express ${countMethod(Express)->Int.toString}, Pickup ${countMethod(
248
+ Pickup,
249
+ )->Int.toString}) — Express placements were auto-shipped on arrival`,
250
+ )
251
+ }
252
+
253
+ let dispatchStandardBatch = async (orders: array<DemoData.order>, ~client: Seed.Client.t) => {
254
+ let dispatched = DemoData.batchDispatched(orders)
255
+ await client->Seed.Client.sendAll(
256
+ dispatched->Array.map(o => DemoCommands.shipOrder(ShipOrder({orderId: o.id}))),
257
+ )
258
+ let standardTotal = orders->Array.filter(o => o.shippingMethod == Standard)->Array.length
259
+ Seed.Runner.report(
260
+ `batch dispatch: ${(dispatched->Array.length)
261
+ ->Int.toString}/${standardTotal->Int.toString} Standard orders shipped, ${(standardTotal -
262
+ dispatched->Array.length)->Int.toString} left pending`,
263
+ )
264
+ dispatched->Array.map(o => o.id)
265
+ }
266
+
267
+ let seedCancellations = async (
268
+ orders: array<DemoData.order>,
269
+ ~client: Seed.Client.t,
270
+ ~dispatched: array<string>,
271
+ ) => {
272
+ let cancellable = DemoData.cancellable(orders, ~dispatched)
273
+ let targets = DemoData.cancelled(cancellable)
274
+ await client->Seed.Client.sendAll(
275
+ targets->Array.map(o => DemoCommands.cancelOrder(CancelOrder({orderId: o.id}))),
276
+ )
277
+ Seed.Runner.report(
278
+ `cancellations: ${(targets->Array.length)->Int.toString} of ${(cancellable->Array.length)
279
+ ->Int.toString} cancellable orders cancelled`,
280
+ )
281
+ }
282
+
283
+ let seedDeactivations = async (customers: array<DemoData.customer>, ~client: Seed.Client.t) => {
284
+ let targets = DemoData.deactivatedCustomers(customers)
285
+ await client->Seed.Client.sendAll(
286
+ targets->Array.map(c => DemoCommands.customer(~id=c.id, Deactivate)),
287
+ )
288
+ Seed.Runner.report(`customers: ${(targets->Array.length)->Int.toString} deactivated`)
289
+ }
290
+
291
+ // ── Summary ─────────────────────────────────────────────────────────────────
292
+
293
+ let summarise = async (~client: Seed.Client.t, ~counts: dict<int>) => {
294
+ let orders = await client->Seed.Client.queryAllNodes(
295
+ ~field="Ordering_Orders",
296
+ ~selection="status shippingMethod",
297
+ )
298
+ let statuses = ["Placed", "Shipped", "Cancelled"]
299
+ let methods = ["Standard", "Express", "Pickup"]
300
+ let countCell = (status, method) =>
301
+ orders
302
+ ->Array.filter(o =>
303
+ o->Seed.Client.nodeString("status") == Some(status) &&
304
+ o->Seed.Client.nodeString("shippingMethod") == Some(method)
305
+ )
306
+ ->Array.length
307
+ let countStatus = status =>
308
+ orders->Array.filter(o => o->Seed.Client.nodeString("status") == Some(status))->Array.length
309
+
310
+ let demand = await client->Seed.Client.queryAllNodes(
311
+ ~field="Catalog_ProductDemands",
312
+ ~selection="name orderCount",
313
+ )
314
+ let orderCountOf = node =>
315
+ switch node->Seed.Client.field("orderCount") {
316
+ | Some(Number(n)) => n->Int.fromFloat
317
+ | _ => 0
318
+ }
319
+ let ranked = demand->Array.toSorted((a, b) => Int.compare(orderCountOf(b), orderCountOf(a)))
320
+ let withDemand = ranked->Array.filter(n => orderCountOf(n) > 0)
321
+ let head =
322
+ ranked
323
+ ->Array.slice(~start=0, ~end=3)
324
+ ->Array.map(n =>
325
+ `${n->Seed.Client.nodeString("name")->Option.getOr("?")} (${orderCountOf(n)->Int.toString})`
326
+ )
327
+ ->Array.join(", ")
328
+
329
+ Seed.Runner.heading("Seeded:")
330
+ Console.log(
331
+ ` order status: ${statuses
332
+ ->Array.map(s => `${s} ${countStatus(s)->Int.toString}`)
333
+ ->Array.join(", ")}`,
334
+ )
335
+ Console.log(` demand head: ${head}`)
336
+ Console.log(
337
+ ` demand spread: ${(withDemand->Array.length)->Int.toString}/${(ranked->Array.length)
338
+ ->Int.toString} products ordered, tail min ${withDemand
339
+ ->Array.last
340
+ ->Option.mapOr(0, orderCountOf)
341
+ ->Int.toString}`,
342
+ )
343
+
344
+ Console.log("")
345
+ Console.log(" status × shippingMethod:")
346
+ Console.log(
347
+ ` ${" "->String.padEnd(10, " ")}${methods
348
+ ->Array.map(m => m->String.padStart(10, " "))
349
+ ->Array.join("")}`,
350
+ )
351
+ statuses->Array.forEach(status =>
352
+ Console.log(
353
+ ` ${status->String.padEnd(10, " ")}${methods
354
+ ->Array.map(m => countCell(status, m)->Int.toString->String.padStart(10, " "))
355
+ ->Array.join("")}`,
356
+ )
357
+ )
358
+
359
+ let observedStatuses = statuses->Array.filter(s => countStatus(s) > 0)
360
+ let spreadWarning = if observedStatuses->Array.length < 3 {
361
+ [
362
+ `the board shows only ${(observedStatuses->Array.length)
363
+ ->Int.toString} of the 3 order statuses (${observedStatuses->Array.join(
364
+ ", ",
365
+ )}) — the shipping-method mix or the batch-dispatch share needs adjusting.`,
366
+ ]
367
+ } else {
368
+ []
369
+ }
370
+ Seed.Runner.warn(Array.concat(spreadWarning, Seed.Runner.unfillableWarnings(~views, ~counts)))
371
+ }
372
+
373
+ // ── Run ─────────────────────────────────────────────────────────────────────
374
+
375
+ // The shared seed run, parameterized only by volume so every data set walks the
376
+ // same phases. Uploads no-op when the connection carries no upload endpoint
377
+ // (a deployment that serves none), keeping `imageUrl` empty rather than failing.
378
+ let run = async (
379
+ connection: Seed.connection,
380
+ ~productCount: int,
381
+ ~customerCount: int,
382
+ ~orderCount: int,
383
+ ): unit => {
384
+ let client = connection.client
385
+
386
+ let built = DemoData.buildProducts(~count=productCount, ())
387
+ let products = if connection.uploadEndpoint == "" {
388
+ Seed.Runner.report(
389
+ "product images: skipped (no upload endpoint / SEED_SKIP_UPLOADS) — imageUrl left empty",
390
+ )
391
+ built
392
+ } else {
393
+ await uploadProductImages(built, ~client, ~uploadEndpoint=connection.uploadEndpoint)
394
+ }
395
+ let customers = DemoData.buildCustomers(~count=customerCount, ())
396
+ let orders = DemoData.buildOrders(products, customers, ~count=orderCount, ())
397
+
398
+ await seedCategories(~client)
399
+ await seedProducts(products, ~client)
400
+ await seedCatalogEdits(products, ~client)
401
+ await seedSupplierFeed(~client)
402
+ await seedCustomers(customers, ~client)
403
+
404
+ // Products reach Ordering asynchronously through the Products extension
405
+ // point; PlaceOrder rejects until the shadow copy lands.
406
+ let expected = Array.concat(products->Array.map(p => p.id), DemoData.importedSkus)
407
+ let available = await client->Seed.Client.waitForIds(
408
+ ~field="Ordering_AvailableProductsByIds",
409
+ ~ids=expected,
410
+ )
411
+ Seed.Runner.report(`ordering: ${available->Int.toString} products available`)
412
+
413
+ await seedOrders(orders, ~client)
414
+ let dispatched = await dispatchStandardBatch(orders, ~client)
415
+ await seedCancellations(orders, ~client, ~dispatched)
416
+ await seedDeactivations(customers, ~client)
417
+
418
+ let counts = await Seed.Runner.verifyViews(client, ~views)
419
+ await summarise(~client, ~counts)
420
+ }
421
+
422
+ // Two sets, so `Seed.Runner.seed` presents a selection: the full demo, and a
423
+ // compact sample for a quick end-to-end check. Both walk identical phases.
424
+ let dataSets: array<Seed.dataSet> = [
425
+ {
426
+ name: "full",
427
+ label: `full — ${DemoData.productCount->Int.toString} products, ${DemoData.customerCount->Int.toString} customers, ${DemoData.orderCount->Int.toString} orders`,
428
+ seed: connection =>
429
+ run(
430
+ connection,
431
+ ~productCount=DemoData.productCount,
432
+ ~customerCount=DemoData.customerCount,
433
+ ~orderCount=DemoData.orderCount,
434
+ ),
435
+ probeViews,
436
+ },
437
+ {
438
+ name: "sample",
439
+ label: "sample — 16 products, 8 customers, 40 orders",
440
+ seed: connection => run(connection, ~productCount=16, ~customerCount=8, ~orderCount=40),
441
+ probeViews,
442
+ },
443
+ ]
@@ -0,0 +1,4 @@
1
+ randjbuildsystem
2
+ DemoCommands
3
+ DemoData
4
+ HybridSeedData
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@reventlessdev/online-shop-hybrid-seed",
3
+ "version": "1.0.0-alpha.10",
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-catalog-spec": "1.0.0-alpha.77",
9
+ "@reventlessdev/online-shop-hybrid-ordering": "1.0.0-alpha.163",
10
+ "@reventlessdev/online-shop-hybrid-ordering-spec": "1.0.0-alpha.77",
11
+ "@reventlessdev/online-shop-hybrid-catalog": "1.0.0-alpha.164",
12
+ "@reventlessdev/reventless-seed": "1.0.0-alpha.4"
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
+ }