@proveanything/smartlinks 2.1.15 → 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
@@ -55,6 +55,35 @@ tag without a chip, with its URL and key; write it, then `ranges.recordTag(scope
55
55
 
56
56
  To find where a chip or code went, `partner.lookupTags(partner.id, { uid })` or `{ code }` searches every set the partner owns and returns each hit's set, tag and range, never a key.
57
57
 
58
+ ## Open ranges and groups
59
+
60
+ A range's count is fixed when it is minted. Two phone-side jobs don't know their count in advance, so a range
61
+ has a `kind`:
62
+
63
+ - **`open`**: blank tags programmed on Sidekick one at a time. `ranges.create(scope, set.id, { kind: 'open', name })`
64
+ reserves only a start index; each `ranges.next()` hands back a free minted code or mints one more, so
65
+ `quantity`/`endIndex` grow as tags are written (`remaining` is null). One open range per set, and nothing can
66
+ be minted behind it (`409 RANGE_OPEN`) until `ranges.close(scope, set.id, range.id)` turns it into an ordinary
67
+ range (an open range that minted nothing is removed instead).
68
+ - **`group`**: tags that already exist, added by tapping, assigned by hand, and moved together later. Assigners may
69
+ make one: `ranges.create(scope, set.id, { kind: 'group', name, intendedCollectionId? })`. Each tap goes through
70
+ `ranges.addMember(scope, set.id, group.id, { code | uid, collectionId, productId? })`, which stamps the tag with
71
+ the group and assigns it with the single-tag rules; `assigned.skipped` says when a tag was left alone (claimed,
72
+ or in a collection you don't administer). `ranges.assign(scope, set.id, group.id, { collectionId })` moves every
73
+ member in one go; `ranges.removeMember` takes one out. Groups have no printer file, instructions, import or
74
+ `next` (`400 NOT_A_RANGE`). Its tags must be codes of the group's set; a chip that is a code elsewhere is
75
+ `409 OTHER_SET`.
76
+
77
+ Status reads `open` or `group` for these (and `assigned` once every member of a group is assigned).
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
+
58
87
  ## The printer file
59
88
 
60
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:
@@ -14952,6 +14981,38 @@ paths:
14952
14981
  description: Unauthorized
14953
14982
  404:
14954
14983
  description: Not found
14984
+ /{groupId}/members:
14985
+ post:
14986
+ tags:
14987
+ - ranges
14988
+ summary: Put an existing tag of the set into a group (by code or chip UID), assigning it if a collection is given.
14989
+ operationId: ranges_addMember
14990
+ security: []
14991
+ parameters:
14992
+ - name: groupId
14993
+ in: path
14994
+ required: true
14995
+ schema:
14996
+ type: string
14997
+ responses:
14998
+ 200:
14999
+ description: Success
15000
+ content:
15001
+ application/json:
15002
+ schema:
15003
+ $ref: "#/components/schemas/AddClaimSetRangeMemberResponse"
15004
+ 400:
15005
+ description: Bad request
15006
+ 401:
15007
+ description: Unauthorized
15008
+ 404:
15009
+ description: Not found
15010
+ requestBody:
15011
+ required: true
15012
+ content:
15013
+ application/json:
15014
+ schema:
15015
+ $ref: "#/components/schemas/AddClaimSetRangeMemberRequest"
14955
15016
  /{rangeId}:
14956
15017
  get:
14957
15018
  tags:
@@ -15041,6 +15102,38 @@ paths:
15041
15102
  application/json:
15042
15103
  schema:
15043
15104
  $ref: "#/components/schemas/AssignClaimSetRangeRequest"
15105
+ /{rangeId}/close:
15106
+ post:
15107
+ tags:
15108
+ - ranges
15109
+ summary: "Close an open range: it becomes a range with the quantity it reached (removed if nothing was minted)."
15110
+ operationId: ranges_close
15111
+ security: []
15112
+ parameters:
15113
+ - name: rangeId
15114
+ in: path
15115
+ required: true
15116
+ schema:
15117
+ type: string
15118
+ responses:
15119
+ 200:
15120
+ description: Success
15121
+ content:
15122
+ application/json:
15123
+ schema:
15124
+ $ref: "#/components/schemas/CloseClaimSetRangeResponse"
15125
+ 400:
15126
+ description: Bad request
15127
+ 401:
15128
+ description: Unauthorized
15129
+ 404:
15130
+ description: Not found
15131
+ requestBody:
15132
+ required: true
15133
+ content:
15134
+ application/json:
15135
+ schema:
15136
+ $ref: "#/components/schemas/ClaimSetScope"
15044
15137
  /{rangeId}/export:
15045
15138
  get:
15046
15139
  tags:
@@ -20764,6 +20857,56 @@ components:
20764
20857
  required:
20765
20858
  - slots
20766
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
20767
20910
  ServerFunctionSl:
20768
20911
  type: object
20769
20912
  properties:
@@ -20772,6 +20915,9 @@ components:
20772
20915
  appRecords:
20773
20916
  type: object
20774
20917
  additionalProperties: true
20918
+ history:
20919
+ type: object
20920
+ additionalProperties: true
20775
20921
  counters:
20776
20922
  $ref: "#/components/schemas/ServerFunctionCounters"
20777
20923
  tags:
@@ -20793,6 +20939,7 @@ components:
20793
20939
  required:
20794
20940
  - bookings
20795
20941
  - appRecords
20942
+ - history
20796
20943
  - counters
20797
20944
  - tags
20798
20945
  - contacts
@@ -21568,6 +21715,9 @@ components:
21568
21715
  enum:
21569
21716
  - caller
21570
21717
  - collection
21718
+ RecordTransactionOp:
21719
+ type: object
21720
+ additionalProperties: true
21571
21721
  PaginatedResponse:
21572
21722
  type: object
21573
21723
  properties:
@@ -21617,6 +21767,19 @@ components:
21617
21767
  expires_at:
21618
21768
  type: object
21619
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
21620
21783
  groupBy:
21621
21784
  type: array
21622
21785
  items:
@@ -25919,6 +26082,8 @@ components:
25919
26082
  type: string
25920
26083
  namespace:
25921
26084
  type: string
26085
+ kind:
26086
+ $ref: "#/components/schemas/ClaimSetRangeKind"
25922
26087
  name:
25923
26088
  type: string
25924
26089
  notes:
@@ -25972,6 +26137,7 @@ components:
25972
26137
  - id
25973
26138
  - claimSetId
25974
26139
  - namespace
26140
+ - kind
25975
26141
  - name
25976
26142
  - notes
25977
26143
  - startIndex
@@ -25995,6 +26161,8 @@ components:
25995
26161
  CreateClaimSetRangeRequest:
25996
26162
  type: object
25997
26163
  properties:
26164
+ kind:
26165
+ $ref: "#/components/schemas/ClaimSetRangeKind"
25998
26166
  name:
25999
26167
  type: string
26000
26168
  quantity:
@@ -26018,7 +26186,6 @@ components:
26018
26186
  type: string
26019
26187
  required:
26020
26188
  - name
26021
- - quantity
26022
26189
  UpdateClaimSetRangeRequest:
26023
26190
  type: object
26024
26191
  properties:
@@ -26062,6 +26229,10 @@ components:
26062
26229
  type: string
26063
26230
  proofId:
26064
26231
  type: string
26232
+ rangeId:
26233
+ type: string
26234
+ groupId:
26235
+ type: string
26065
26236
  programmedAt:
26066
26237
  type: string
26067
26238
  scans:
@@ -26075,6 +26246,8 @@ components:
26075
26246
  - productId
26076
26247
  - batchId
26077
26248
  - proofId
26249
+ - rangeId
26250
+ - groupId
26078
26251
  - programmedAt
26079
26252
  - scans
26080
26253
  ClaimSetRangeTagsPage:
@@ -26103,6 +26276,41 @@ components:
26103
26276
  required:
26104
26277
  - url
26105
26278
  - remaining
26279
+ AddClaimSetRangeMemberRequest:
26280
+ type: object
26281
+ properties:
26282
+ code:
26283
+ type: string
26284
+ uid:
26285
+ type: string
26286
+ collectionId:
26287
+ type: string
26288
+ productId:
26289
+ type: string
26290
+ batchId:
26291
+ type: string
26292
+ data:
26293
+ type: object
26294
+ additionalProperties: true
26295
+ clear:
26296
+ type: boolean
26297
+ AddClaimSetRangeMemberResponse:
26298
+ type: object
26299
+ properties:
26300
+ tag:
26301
+ $ref: "#/components/schemas/ClaimSetRangeTag"
26302
+ range:
26303
+ $ref: "#/components/schemas/ClaimSetRange"
26304
+ assigned:
26305
+ type: object
26306
+ additionalProperties: true
26307
+ movedFrom:
26308
+ type: string
26309
+ required:
26310
+ - tag
26311
+ - range
26312
+ - assigned
26313
+ - movedFrom
26106
26314
  ImportClaimSetRangeUidsRequest:
26107
26315
  type: object
26108
26316
  properties:
@@ -26272,12 +26480,15 @@ components:
26272
26480
  $ref: "#/components/schemas/ClaimSetRangeTag"
26273
26481
  range:
26274
26482
  $ref: "#/components/schemas/ClaimSetRange"
26483
+ group:
26484
+ $ref: "#/components/schemas/ClaimSetRange"
26275
26485
  required:
26276
26486
  - namespace
26277
26487
  - claimSetId
26278
26488
  - set
26279
26489
  - tag
26280
26490
  - range
26491
+ - group
26281
26492
  CreatePartnerClaimSetRequest:
26282
26493
  type: object
26283
26494
  properties:
@@ -26303,6 +26514,12 @@ components:
26303
26514
  - ntag424
26304
26515
  - ntag21x
26305
26516
  - qr
26517
+ ClaimSetRangeKind:
26518
+ type: string
26519
+ enum:
26520
+ - range
26521
+ - open
26522
+ - group
26306
26523
  ClaimSetRangeStatus:
26307
26524
  type: string
26308
26525
  enum:
@@ -26311,6 +26528,11 @@ components:
26311
26528
  - sent
26312
26529
  - received
26313
26530
  - assigned
26531
+ - open
26532
+ - group
26533
+ CloseClaimSetRangeResponse:
26534
+ type: object
26535
+ additionalProperties: true
26314
26536
  ClaimSetImportResult:
26315
26537
  type: string
26316
26538
  enum:
@@ -26324,6 +26546,92 @@ components:
26324
26546
  - duplicate_uid
26325
26547
  - uid_in_use
26326
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
26327
26635
  Collection:
26328
26636
  type: object
26329
26637
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proveanything/smartlinks",
3
- "version": "2.1.15",
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",