@proveanything/smartlinks 2.1.16 → 2.1.17

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.
@@ -514,6 +514,10 @@ Prefer the platform primitives above over a dependency; when you do need one, pi
514
514
  | Read ANOTHER app's data (as the caller may see it) | `ctx.sl.app('other-app').data.get()` — caller-authority, this collection, public-filtered, read-only | `sl:data:read` |
515
515
  | Guarantee a UNIQUE claim (one entry per tag, one-per-user, idempotency key, a number nobody else holds) | `ctx.sl.appRecords.claim({ …, singletonPer, customId })` → `{ record, created, conflict? }` — see below | `sl:records:write` |
516
516
  | Hand out the next number (tickets, order numbers) | `ctx.sl.counters.next('tickets')` — atomic, distinct per caller | `sl:data:write` |
517
+ | Change numbers in a record (stock, counts, balances) without losing updates | `ctx.sl.appRecords.adjust(id, { qty: -5 }, { atLeast: { qty: 0 } })` — in place, conditions checked after the change | `sl:records:write` |
518
+ | Several record writes that land together or not at all (a stock move) | `ctx.sl.transaction([ops], { idempotencyKey })` — see below | `sl:records:write` |
519
+ | Totals: stock per location, forecast per SKU | `ctx.sl.appRecords.aggregate({ groupBy: ['data.location'], metrics: ['sum:qty'] })` | `sl:records:read` |
520
+ | Keep a history of what happened (stock moves, counts) without growing your records | `ctx.sl.history.log('stock.moved', {…})`, or a `log` op in a transaction; read with `ctx.sl.history.query({…})` | `sl:history:write` / `sl:history:read` |
517
521
  | Check a tapped tag is real | `ctx.sl.tags.resolve(proofId)` → `{ id, claimSetId, code }` or `null` | `sl:tags:read` |
518
522
  | Put a visitor's details on a contact | `ctx.sl.contacts.upsert({ email, firstName, … })` → `{ contactId, created }` (fills empty fields only) | `sl:contacts:write` |
519
523
  | Record "they did X" | `ctx.sl.interactions.record({ interactionId, contactId, outcome, metadata })` | `sl:interactions:write` |
@@ -579,6 +583,90 @@ reads the last value handed out (`null` before the first). Name a new counter to
579
583
  `customId: String(n)`. If the claim says `conflict: 'customId'`, take `next()` again. A failed or raced
580
584
  attempt only leaves a gap — never a duplicate.
581
585
 
586
+ ### `appRecords.adjust(id, by, { atLeast?, atMost? })` — numbers that never lose an update
587
+
588
+ Adds to numbers in a record's `data` in ONE statement, so twenty scanners taking from the same bin at
589
+ once each land: no read-then-write, no lost counts. A missing value counts as 0. `atLeast` / `atMost` are
590
+ checked on the values **after** the change, inside the same statement:
591
+
592
+ ```ts
593
+ // Take 5 from bin A — refused (nothing changes) if it would go below zero.
594
+ await ctx.sl.appRecords.adjust(binA, { qty: -5 }, { atLeast: { qty: 0 } })
595
+ ```
596
+
597
+ Refusals carry `code`: `NOT_FOUND` (not this app's record in this collection, or deleted), `NOT_A_NUMBER`
598
+ (the key holds something else), `CONDITION_FAILED` (409, with the `current` values). Only top-level data
599
+ keys; a key named only in a condition is checked as it stands.
600
+
601
+ ### `transaction(ops, { idempotencyKey? })` — all or nothing
602
+
603
+ Several writes as ONE transaction: every op lands, or none does. The classic: take stock out of bin A,
604
+ put it on order line B, and log the move.
605
+
606
+ ```ts
607
+ const { results, replayed } = await ctx.sl.transaction([
608
+ { op: 'adjust', recordId: binA, by: { qty: -5 }, atLeast: { qty: 0 } },
609
+ { op: 'adjust', recordId: orderLineB, by: { picked: 5 } },
610
+ { op: 'create', record: { recordType: 'movement', data: { from: binA, to: orderLineB, qty: 5 } } },
611
+ ], { idempotencyKey: scanId })
612
+ ```
613
+
614
+ - Ops: `create` (`record`), `update` (`recordId`, `fields`), `adjust` (`recordId`, `by`, `atLeast?`,
615
+ `atMost?`), `delete` (`recordId`), `log` (`type`, `data`, `entityId?`: a history event, see below).
616
+ Up to 50, run in order. This app's records in this collection only.
617
+ - A failing op rolls everything back. The error says which op (`opIndex`) and why (its `code`).
618
+ - Change events and realtime updates go out only after the transaction commits, never for a rollback.
619
+ - **`idempotencyKey`** ("this was already done"): recorded in the same commit, with what was written
620
+ (ids only). The same key again within 7 days doesn't write; it returns those records as they are now,
621
+ with `replayed: true`. So a handheld that retries after a dropped signal never moves stock twice. Two
622
+ calls with one key at the same moment: one commits, the other replays it. A transaction that failed
623
+ leaves its key free, so a retry can still succeed.
624
+ - Use the scan's or the request's own id as the key (one key = one intended change).
625
+ - Browser code never writes stock directly: a warehouse screen calls your function.
626
+
627
+ ### `history` — what happened, kept without growing your records
628
+
629
+ Keep **current state** in records (a bin's quantity, an order's status) and **what happened** in the app's
630
+ history: append-only events in SmartLinks' analytics store, kept for good, cheap however many there
631
+ are. Picking every order line for years would bloat records; as history it costs nothing operational.
632
+
633
+ ```ts
634
+ // In a transaction: commits with the stock change, or not at all.
635
+ await ctx.sl.transaction([
636
+ { op: 'adjust', recordId: binA, by: { qty: -5 }, atLeast: { qty: 0 } },
637
+ { op: 'log', type: 'stock.moved', entityId: binA, data: { sku: 'EDP-50', from: 'A1', to: orderId, qty: 5 } },
638
+ ], { idempotencyKey: scanId })
639
+
640
+ // On its own:
641
+ await ctx.sl.history.log('count.taken', { bin: 'A1', counted: 40, expected: 42 }, { entityId: binA })
642
+
643
+ // Reading back: this app's events in this collection, newest first (default last 30 days, max 400).
644
+ const moves = await ctx.sl.history.query({ types: 'stock.moved', entityId: binA, limit: 20 })
645
+ // → [{ id, at, type, entityId, data, actorType, actorId }]
646
+ ```
647
+
648
+ - `type`: lowercase letters, digits and `. _ -` (`stock.moved`). `data`: up to 32 KB, so **log a summary**:
649
+ one line for "5 of EDP-50", not five; one line for an RFID count, not one per tag.
650
+ - Delivery takes seconds and is retried until it lands; a logged event is never lost. A query reads the
651
+ analytics store, so expect about a second: right for a history panel, not for a hot path.
652
+ - The person who called your function is recorded as the actor.
653
+
654
+ ### `appRecords.aggregate(params)` — totals
655
+
656
+ Counts and sums over your app's records, worked out in the database:
657
+
658
+ ```ts
659
+ await ctx.sl.appRecords.aggregate({
660
+ groupBy: ['data.location', 'data.sku'], // columns, or top-level data keys
661
+ metrics: ['count', 'sum:qty'], // sum: / avg: / min: / max: over data numbers
662
+ filters: { record_type: 'bin', data: { warehouse: 'north' }, dataRange: { qty: { gt: 0 } } },
663
+ })
664
+ // → { groups: [{ 'data.location': 'A1', 'data.sku': 'EDP-50', count: 3, sum_qty: 42 }, …] }
665
+ ```
666
+
667
+ Non-numbers are ignored by `sum`/`avg`/`min`/`max`. The same request works from the browser through
668
+ `app.records.aggregate` (admin) — see [app-objects.md](app-objects.md).
669
+
582
670
  ### `tags.resolve(id)` — is this tap real?
583
671
 
584
672
  A tap hands your app the virtual-proof id `<claimSetId>-<code>`. `resolve` returns
@@ -76,6 +76,14 @@ has a `kind`:
76
76
 
77
77
  Status reads `open` or `group` for these (and `assigned` once every member of a group is assigned).
78
78
 
79
+ ## When a tag fails on the phone
80
+
81
+ `errors.report({ app: 'sidekick', operation: 'tag.program', errorCode: 'TAG_LOST', message, entityType: 'tag',
82
+ entityId: chipUid, labels: { claimSetId, code, failedStep }, context: { steps, timings, hostLog } })` keeps what
83
+ the phone saw in the platform's one error log (never a key: fields that look like one are dropped).
84
+ `partner.tagAttempts(partner.id, chipUid)` and the platform's tag lookup read it back, newest first, so a
85
+ failing chip can be understood by its UID without the phone's console.
86
+
79
87
  ## The printer file
80
88
 
81
89
  One row per tag, in index order. The manufacturer fills in `UID` and sends the same file back; rows are
package/openapi.yaml CHANGED
@@ -9141,6 +9141,35 @@ paths:
9141
9141
  description: Unauthorized
9142
9142
  404:
9143
9143
  description: Not found
9144
+ /admin/partner/{partnerId}/tags/attempts:
9145
+ get:
9146
+ tags:
9147
+ - partner
9148
+ summary: "What phones reported about one chip (errors.report with entityType 'tag'), newest first: a tag that failed to program or register, with the host's NFC log in context."
9149
+ operationId: partner_tagAttempts
9150
+ security:
9151
+ - bearerAuth: []
9152
+ parameters:
9153
+ - name: partnerId
9154
+ in: path
9155
+ required: true
9156
+ schema:
9157
+ type: string
9158
+ responses:
9159
+ 200:
9160
+ description: Success
9161
+ content:
9162
+ application/json:
9163
+ schema:
9164
+ type: array
9165
+ items:
9166
+ $ref: "#/components/schemas/ClientErrorEvent"
9167
+ 400:
9168
+ description: Bad request
9169
+ 401:
9170
+ description: Unauthorized
9171
+ 404:
9172
+ description: Not found
9144
9173
  /admin/partner/{partnerId}/tags/lookup:
9145
9174
  get:
9146
9175
  tags:
@@ -20828,6 +20857,56 @@ components:
20828
20857
  required:
20829
20858
  - slots
20830
20859
  - blocks
20860
+ AppHistoryEvent:
20861
+ type: object
20862
+ properties:
20863
+ id:
20864
+ type: string
20865
+ at:
20866
+ type: string
20867
+ type:
20868
+ type: string
20869
+ entityId:
20870
+ type: string
20871
+ data:
20872
+ type: object
20873
+ additionalProperties: true
20874
+ actorType:
20875
+ type: string
20876
+ enum:
20877
+ - user
20878
+ - contact
20879
+ actorId:
20880
+ type: string
20881
+ required:
20882
+ - id
20883
+ - at
20884
+ - type
20885
+ - entityId
20886
+ - data
20887
+ - actorType
20888
+ - actorId
20889
+ AppHistoryQuery:
20890
+ type: object
20891
+ properties:
20892
+ types:
20893
+ type: array
20894
+ items:
20895
+ type: object
20896
+ additionalProperties: true
20897
+ entityId:
20898
+ type: string
20899
+ data:
20900
+ type: object
20901
+ additionalProperties:
20902
+ type: object
20903
+ additionalProperties: true
20904
+ from:
20905
+ type: string
20906
+ to:
20907
+ type: string
20908
+ limit:
20909
+ type: number
20831
20910
  ServerFunctionSl:
20832
20911
  type: object
20833
20912
  properties:
@@ -20836,6 +20915,9 @@ components:
20836
20915
  appRecords:
20837
20916
  type: object
20838
20917
  additionalProperties: true
20918
+ history:
20919
+ type: object
20920
+ additionalProperties: true
20839
20921
  counters:
20840
20922
  $ref: "#/components/schemas/ServerFunctionCounters"
20841
20923
  tags:
@@ -20857,6 +20939,7 @@ components:
20857
20939
  required:
20858
20940
  - bookings
20859
20941
  - appRecords
20942
+ - history
20860
20943
  - counters
20861
20944
  - tags
20862
20945
  - contacts
@@ -21632,6 +21715,9 @@ components:
21632
21715
  enum:
21633
21716
  - caller
21634
21717
  - collection
21718
+ RecordTransactionOp:
21719
+ type: object
21720
+ additionalProperties: true
21635
21721
  PaginatedResponse:
21636
21722
  type: object
21637
21723
  properties:
@@ -21681,6 +21767,19 @@ components:
21681
21767
  expires_at:
21682
21768
  type: object
21683
21769
  additionalProperties: true
21770
+ updated_at:
21771
+ type: object
21772
+ additionalProperties: true
21773
+ data:
21774
+ type: object
21775
+ additionalProperties:
21776
+ type: object
21777
+ additionalProperties: true
21778
+ dataRange:
21779
+ type: object
21780
+ additionalProperties:
21781
+ type: object
21782
+ additionalProperties: true
21684
21783
  groupBy:
21685
21784
  type: array
21686
21785
  items:
@@ -26381,12 +26480,15 @@ components:
26381
26480
  $ref: "#/components/schemas/ClaimSetRangeTag"
26382
26481
  range:
26383
26482
  $ref: "#/components/schemas/ClaimSetRange"
26483
+ group:
26484
+ $ref: "#/components/schemas/ClaimSetRange"
26384
26485
  required:
26385
26486
  - namespace
26386
26487
  - claimSetId
26387
26488
  - set
26388
26489
  - tag
26389
26490
  - range
26491
+ - group
26390
26492
  CreatePartnerClaimSetRequest:
26391
26493
  type: object
26392
26494
  properties:
@@ -26444,6 +26546,92 @@ components:
26444
26546
  - duplicate_uid
26445
26547
  - uid_in_use
26446
26548
  - uid_conflict
26549
+ ClientErrorReport:
26550
+ type: object
26551
+ properties:
26552
+ app:
26553
+ type: string
26554
+ appVersion:
26555
+ type: string
26556
+ collectionId:
26557
+ type: string
26558
+ operation:
26559
+ type: string
26560
+ severity:
26561
+ type: string
26562
+ enum:
26563
+ - debug
26564
+ - info
26565
+ - warning
26566
+ - error
26567
+ - critical
26568
+ errorCode:
26569
+ type: string
26570
+ errorType:
26571
+ type: string
26572
+ message:
26573
+ type: string
26574
+ stack:
26575
+ type: string
26576
+ entityType:
26577
+ type: string
26578
+ entityId:
26579
+ type: string
26580
+ labels:
26581
+ type: object
26582
+ additionalProperties: true
26583
+ context:
26584
+ type: object
26585
+ additionalProperties: true
26586
+ refId:
26587
+ type: string
26588
+ correlationId:
26589
+ type: string
26590
+ ClientErrorEvent:
26591
+ type: object
26592
+ properties:
26593
+ id:
26594
+ type: string
26595
+ time:
26596
+ type: string
26597
+ event_category:
26598
+ type: string
26599
+ event_action:
26600
+ type: string
26601
+ log_level:
26602
+ type: string
26603
+ error_code:
26604
+ type: string
26605
+ error_message:
26606
+ type: string
26607
+ service_name:
26608
+ type: string
26609
+ service_version:
26610
+ type: string
26611
+ collection_id:
26612
+ type: string
26613
+ user_id:
26614
+ type: string
26615
+ labels:
26616
+ type: object
26617
+ additionalProperties: true
26618
+ context:
26619
+ type: object
26620
+ additionalProperties: true
26621
+ required:
26622
+ - id
26623
+ - time
26624
+ - event_category
26625
+ - event_action
26626
+ - log_level
26627
+ - error_code
26628
+ - error_message
26629
+ - service_name
26630
+ - service_version
26631
+ - collection_id
26632
+ - user_id
26633
+ - labels
26634
+ - context
26447
26635
  Collection:
26448
26636
  type: object
26449
26637
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.1.16",
3
+ "version": "2.1.17",
4
4
  "description": "Official JavaScript/TypeScript SDK for the Smartlinks API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",