@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.
- package/dist/api/errors.d.ts +10 -0
- package/dist/api/errors.js +17 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/partner.d.ts +7 -1
- package/dist/api/partner.js +9 -0
- package/dist/docs/API_SUMMARY.md +96 -5
- package/dist/docs/app-objects.md +23 -0
- package/dist/docs/server-functions.md +88 -0
- package/dist/docs/tag-ranges.md +8 -0
- package/dist/openapi.yaml +188 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.js +123 -1
- package/dist/types/appManifest.d.ts +87 -0
- package/dist/types/appObjects.d.ts +22 -1
- package/dist/types/claimSet.d.ts +2 -0
- package/dist/types/clientLog.d.ts +46 -0
- package/dist/types/clientLog.js +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/docs/API_SUMMARY.md +96 -5
- package/docs/app-objects.md +23 -0
- package/docs/server-functions.md +88 -0
- package/docs/tag-ranges.md +8 -0
- package/openapi.yaml +188 -0
- package/package.json +1 -1
package/dist/testing/index.js
CHANGED
|
@@ -77,7 +77,7 @@ function allowsSl(p, resource, op) {
|
|
|
77
77
|
}
|
|
78
78
|
/** A fresh in-memory store — pass the same one to several test contexts to simulate concurrent callers. */
|
|
79
79
|
export function createFunctionTestMemory() {
|
|
80
|
-
return { records: [], counters: {}, contacts: [], interactions: [], bookings: createBookingsMemory() };
|
|
80
|
+
return { records: [], counters: {}, contacts: [], interactions: [], bookings: createBookingsMemory(), transactions: {}, history: [] };
|
|
81
81
|
}
|
|
82
82
|
const stub = (method, args) => ({ __stub: true, method, args });
|
|
83
83
|
/**
|
|
@@ -101,6 +101,9 @@ export function createFunctionTestContext(opts) {
|
|
|
101
101
|
return fn ? fn(...args) : stub(`${resource}.${name}`, args);
|
|
102
102
|
};
|
|
103
103
|
const mem = opts.memory || createFunctionTestMemory();
|
|
104
|
+
// A memory built before history / transactions existed still works.
|
|
105
|
+
mem.history = mem.history || [];
|
|
106
|
+
mem.transactions = mem.transactions || {};
|
|
104
107
|
const ar = impl.appRecords || {};
|
|
105
108
|
const pr = impl.products || {};
|
|
106
109
|
const at = impl.attestations || {};
|
|
@@ -147,6 +150,60 @@ export function createFunctionTestContext(opts) {
|
|
|
147
150
|
return null; Object.assign(r, fields); return Object.assign({}, r); },
|
|
148
151
|
delete: async (id) => { const r = mem.records.find((x) => x.id === id); if (r)
|
|
149
152
|
r.deletedAt = new Date().toISOString(); return {}; },
|
|
153
|
+
// Numbers in data changed in place, with atLeast / atMost checked after the change (as the platform).
|
|
154
|
+
adjust: async (id, by = {}, opts = {}) => {
|
|
155
|
+
var _a, _b;
|
|
156
|
+
const r = mem.records.find((x) => x.id === id && !x.deletedAt);
|
|
157
|
+
if (!r)
|
|
158
|
+
throw Object.assign(new Error('Record not found'), { code: 'NOT_FOUND', statusCode: 404 });
|
|
159
|
+
const data = r.data || {};
|
|
160
|
+
const keys = [...new Set([...Object.keys(by), ...Object.keys(opts.atLeast || {}), ...Object.keys(opts.atMost || {})])];
|
|
161
|
+
const bad = keys.find((k) => data[k] != null && typeof data[k] !== 'number');
|
|
162
|
+
if (bad)
|
|
163
|
+
throw Object.assign(new Error(`data.${bad} isn't a number, so it can't be adjusted`), { code: 'NOT_A_NUMBER', statusCode: 400, key: bad });
|
|
164
|
+
const next = {};
|
|
165
|
+
for (const k of keys)
|
|
166
|
+
next[k] = ((_a = data[k]) !== null && _a !== void 0 ? _a : 0) + ((_b = by[k]) !== null && _b !== void 0 ? _b : 0);
|
|
167
|
+
const broken = Object.entries(opts.atLeast || {}).some(([k, n]) => next[k] < n) || Object.entries(opts.atMost || {}).some(([k, n]) => next[k] > n);
|
|
168
|
+
if (broken)
|
|
169
|
+
throw Object.assign(new Error('The change would break its condition (atLeast / atMost)'), { code: 'CONDITION_FAILED', statusCode: 409, current: Object.fromEntries(keys.map((k) => { var _a; return [k, (_a = data[k]) !== null && _a !== void 0 ? _a : 0]; })) });
|
|
170
|
+
r.data = Object.assign(Object.assign({}, data), Object.fromEntries(Object.keys(by).map((k) => [k, next[k]])));
|
|
171
|
+
return Object.assign({}, r);
|
|
172
|
+
},
|
|
173
|
+
// count / sum: / avg: / min: / max: over data numbers, grouped by top-level fields or data keys
|
|
174
|
+
// ('data.location'); filters.data (exact) as the platform. A simple stand-in for tests.
|
|
175
|
+
aggregate: async (params = {}) => {
|
|
176
|
+
const { groupBy = [], metrics = ['count'], filters = {} } = params || {};
|
|
177
|
+
const rows = mem.records.filter((r) => !r.deletedAt &&
|
|
178
|
+
(!filters.record_type || r.recordType === filters.record_type) &&
|
|
179
|
+
(!filters.status || r.status === filters.status) &&
|
|
180
|
+
Object.entries(filters.data || {}).every(([k, v]) => (Array.isArray(v) ? v.map(String) : [String(v)]).includes(String((r.data || {})[k]))));
|
|
181
|
+
const field = (r, f) => (f.startsWith('data.') ? (r.data || {})[f.slice(5)] : r[f === 'record_type' ? 'recordType' : f]);
|
|
182
|
+
const measure = (set) => {
|
|
183
|
+
const out = {};
|
|
184
|
+
for (const m of metrics) {
|
|
185
|
+
if (m === 'count') {
|
|
186
|
+
out.count = set.length;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const [fn, key] = String(m).split(':');
|
|
190
|
+
const nums = set.map((r) => (r.data || {})[key]).filter((v) => typeof v === 'number');
|
|
191
|
+
const sum = nums.reduce((a, b) => a + b, 0);
|
|
192
|
+
out[`${fn}_${key}`] = !nums.length ? null : fn === 'sum' ? sum : fn === 'avg' ? sum / nums.length : fn === 'min' ? Math.min(...nums) : Math.max(...nums);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
};
|
|
196
|
+
if (!groupBy.length)
|
|
197
|
+
return measure(rows);
|
|
198
|
+
const buckets = new Map();
|
|
199
|
+
for (const r of rows) {
|
|
200
|
+
const key = JSON.stringify(groupBy.map((f) => { var _a; return (_a = field(r, f)) !== null && _a !== void 0 ? _a : null; }));
|
|
201
|
+
if (!buckets.has(key))
|
|
202
|
+
buckets.set(key, []);
|
|
203
|
+
buckets.get(key).push(r);
|
|
204
|
+
}
|
|
205
|
+
return { groups: [...buckets].map(([key, set]) => (Object.assign(Object.assign({}, Object.fromEntries(groupBy.map((f, i) => [f, JSON.parse(key)[i]]))), measure(set)))) };
|
|
206
|
+
},
|
|
150
207
|
// Simple equality filter on top-level fields (recordType, proofId, customId, status, …).
|
|
151
208
|
query: async (params = {}) => {
|
|
152
209
|
const _a = params || {}, { limit, offset, sort } = _a, where = __rest(_a, ["limit", "offset", "sort"]);
|
|
@@ -158,6 +215,60 @@ export function createFunctionTestContext(opts) {
|
|
|
158
215
|
return { data, pagination: { total: all.length, limit: size, offset: from, hasMore: from + data.length < all.length } };
|
|
159
216
|
},
|
|
160
217
|
};
|
|
218
|
+
// The app's history, in memory (newest first when read, like the platform).
|
|
219
|
+
let nextEventId = 1;
|
|
220
|
+
const memLog = (type, data = {}, opts = {}) => {
|
|
221
|
+
var _a;
|
|
222
|
+
if (typeof type !== 'string' || !/^[a-z][a-z0-9._-]{0,63}$/.test(type))
|
|
223
|
+
throw Object.assign(new Error('history type: lowercase letters, digits and . _ - (e.g. "stock.moved")'), { code: 'INVALID_INPUT', statusCode: 400 });
|
|
224
|
+
const ev = { id: `ev-${nextEventId++}`, at: new Date().toISOString(), type, entityId: (_a = opts.entityId) !== null && _a !== void 0 ? _a : null, data: data || {}, actorType: null, actorId: null };
|
|
225
|
+
mem.history.push(ev);
|
|
226
|
+
return { id: ev.id };
|
|
227
|
+
};
|
|
228
|
+
const memHistoryQuery = async (params = {}) => {
|
|
229
|
+
const types = params.types == null ? null : (Array.isArray(params.types) ? params.types : [params.types]);
|
|
230
|
+
return mem.history
|
|
231
|
+
.filter((e) => (!types || types.includes(e.type)) && (params.entityId == null || e.entityId === params.entityId) &&
|
|
232
|
+
Object.entries(params.data || {}).every(([k, v]) => (Array.isArray(v) ? v.map(String) : [String(v)]).includes(String(e.data[k]))))
|
|
233
|
+
.slice().reverse().slice(0, params.limit || 100).map((e) => (Object.assign({}, e)));
|
|
234
|
+
};
|
|
235
|
+
// All ops or none, like the platform: records are restored when an op fails; a repeat of an
|
|
236
|
+
// idempotencyKey replays the first results.
|
|
237
|
+
const memTransaction = async (ops, opts = {}) => {
|
|
238
|
+
const key = opts.idempotencyKey;
|
|
239
|
+
if (key && mem.transactions[key])
|
|
240
|
+
return { results: mem.transactions[key], replayed: true };
|
|
241
|
+
const before = JSON.parse(JSON.stringify(mem.records));
|
|
242
|
+
const historyBefore = mem.history.length;
|
|
243
|
+
const results = [];
|
|
244
|
+
for (let i = 0; i < ops.length; i++) {
|
|
245
|
+
const o = ops[i] || {};
|
|
246
|
+
try {
|
|
247
|
+
if (o.op === 'create')
|
|
248
|
+
results.push(await memRecords.create(o.record || {}));
|
|
249
|
+
else if (o.op === 'update')
|
|
250
|
+
results.push(await memRecords.update(o.recordId, o.fields || {}));
|
|
251
|
+
else if (o.op === 'adjust')
|
|
252
|
+
results.push(await memRecords.adjust(o.recordId, o.by, { atLeast: o.atLeast, atMost: o.atMost }));
|
|
253
|
+
else if (o.op === 'delete') {
|
|
254
|
+
await memRecords.delete(o.recordId);
|
|
255
|
+
results.push({ deleted: o.recordId });
|
|
256
|
+
}
|
|
257
|
+
else if (o.op === 'log')
|
|
258
|
+
results.push({ logged: memLog(o.type, o.data, { entityId: o.entityId }).id });
|
|
259
|
+
else
|
|
260
|
+
throw Object.assign(new Error(`ops[${i}].op must be create, update, adjust, delete or log`), { code: 'INVALID_INPUT', statusCode: 400 });
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
mem.records.splice(0, mem.records.length, ...before);
|
|
264
|
+
mem.history.splice(historyBefore);
|
|
265
|
+
throw Object.assign(err, { opIndex: i });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (key)
|
|
269
|
+
mem.transactions[key] = results;
|
|
270
|
+
return { results, replayed: false };
|
|
271
|
+
};
|
|
161
272
|
const recordsFn = (name) => ar[name] ? ar[name].bind(ar) : (impl.appRecords ? undefined : memRecords[name]);
|
|
162
273
|
const sl = {
|
|
163
274
|
appRecords: {
|
|
@@ -169,6 +280,17 @@ export function createFunctionTestContext(opts) {
|
|
|
169
280
|
get: gated('records', 'read', recordsFn('get'), 'get'),
|
|
170
281
|
query: gated('records', 'read', recordsFn('query'), 'query'),
|
|
171
282
|
listTypes: gated('records', 'read', recordsFn('listTypes'), 'listTypes'),
|
|
283
|
+
adjust: gated('records', 'write', recordsFn('adjust'), 'adjust'),
|
|
284
|
+
aggregate: gated('records', 'read', recordsFn('aggregate'), 'aggregate'),
|
|
285
|
+
},
|
|
286
|
+
transaction: gated('records', 'write', impl.transaction ? impl.transaction : (impl.appRecords ? undefined : async (ops, opts) => {
|
|
287
|
+
if (Array.isArray(ops) && ops.some((o) => o && o.op === 'log') && !allowsSl(parsed, 'history', 'write'))
|
|
288
|
+
throw new CapabilityError('sl:history:write');
|
|
289
|
+
return memTransaction(ops, opts);
|
|
290
|
+
}), 'transaction'),
|
|
291
|
+
history: {
|
|
292
|
+
log: gated('history', 'write', async (type, data, opts) => memLog(type, data, opts), 'log'),
|
|
293
|
+
query: gated('history', 'read', memHistoryQuery, 'query'),
|
|
172
294
|
},
|
|
173
295
|
products: {
|
|
174
296
|
get: gated('products', 'read', pr.get && pr.get.bind(pr), 'get'),
|
|
@@ -490,6 +490,53 @@ export interface ServerFunctionBookings {
|
|
|
490
490
|
}>;
|
|
491
491
|
};
|
|
492
492
|
}
|
|
493
|
+
/** One write in ctx.sl.transaction. */
|
|
494
|
+
export type RecordTransactionOp = {
|
|
495
|
+
op: 'create';
|
|
496
|
+
record: Record<string, any>;
|
|
497
|
+
} | {
|
|
498
|
+
op: 'update';
|
|
499
|
+
recordId: string;
|
|
500
|
+
fields: Record<string, any>;
|
|
501
|
+
} | {
|
|
502
|
+
op: 'adjust';
|
|
503
|
+
recordId: string;
|
|
504
|
+
by: Record<string, number>;
|
|
505
|
+
atLeast?: Record<string, number>;
|
|
506
|
+
atMost?: Record<string, number>;
|
|
507
|
+
} | {
|
|
508
|
+
op: 'delete';
|
|
509
|
+
recordId: string;
|
|
510
|
+
}
|
|
511
|
+
/** An event in the app's history (BigQuery): commits with the other ops, delivered after. Needs sl:history:write. */
|
|
512
|
+
| {
|
|
513
|
+
op: 'log';
|
|
514
|
+
type: string;
|
|
515
|
+
data?: Record<string, any>;
|
|
516
|
+
entityId?: string;
|
|
517
|
+
};
|
|
518
|
+
/** One event from an app's history. */
|
|
519
|
+
export interface AppHistoryEvent {
|
|
520
|
+
id: string;
|
|
521
|
+
at: string;
|
|
522
|
+
type: string;
|
|
523
|
+
entityId: string | null;
|
|
524
|
+
data: Record<string, any>;
|
|
525
|
+
actorType: 'user' | 'contact' | null;
|
|
526
|
+
actorId: string | null;
|
|
527
|
+
}
|
|
528
|
+
/** Reading an app's history: its own events in this collection, newest first. */
|
|
529
|
+
export interface AppHistoryQuery {
|
|
530
|
+
types?: string | string[];
|
|
531
|
+
entityId?: string;
|
|
532
|
+
/** Exact match on top-level data keys (one value or several). */
|
|
533
|
+
data?: Record<string, string | number | boolean | Array<string | number | boolean>>;
|
|
534
|
+
/** ISO dates. Default: the last 30 days; at most 400 days. */
|
|
535
|
+
from?: string;
|
|
536
|
+
to?: string;
|
|
537
|
+
/** Default 100, at most 1000. */
|
|
538
|
+
limit?: number;
|
|
539
|
+
}
|
|
493
540
|
export interface ServerFunctionSl {
|
|
494
541
|
/** THIS app's bookings, slots and blocks (`sl:bookings:read` / `sl:bookings:write`). */
|
|
495
542
|
bookings: ServerFunctionBookings;
|
|
@@ -502,8 +549,48 @@ export interface ServerFunctionSl {
|
|
|
502
549
|
*/
|
|
503
550
|
appRecords: {
|
|
504
551
|
claim(fields: Record<string, any>): Promise<AppRecordClaimResult>;
|
|
552
|
+
/**
|
|
553
|
+
* Numbers in a record's `data`, changed in place in one statement: two scanners on one bin never lose
|
|
554
|
+
* a count. `by` `{ qty: -5 }` (missing counts as 0). `atLeast` / `atMost` are checked on the values
|
|
555
|
+
* AFTER the change: `{ atLeast: { qty: 0 } }` never takes a bin below zero. Refusals carry `code`:
|
|
556
|
+
* `NOT_FOUND`, `NOT_A_NUMBER`, `CONDITION_FAILED` (with `current` values). → the updated record.
|
|
557
|
+
*/
|
|
558
|
+
adjust(recordId: string, by: Record<string, number>, opts?: {
|
|
559
|
+
atLeast?: Record<string, number>;
|
|
560
|
+
atMost?: Record<string, number>;
|
|
561
|
+
}): Promise<any>;
|
|
562
|
+
/** Totals over THIS app's records: count, sum/avg/min/max of data numbers, grouped by data keys. */
|
|
563
|
+
aggregate(params?: import('./appObjects.js').AggregateRequest): Promise<import('./appObjects.js').AggregateResponse & Record<string, unknown>>;
|
|
505
564
|
[method: string]: any;
|
|
506
565
|
};
|
|
566
|
+
/**
|
|
567
|
+
* Several record writes that land together or not at all (take 5 from bin A AND add 5 to order line B
|
|
568
|
+
* AND log the move). Needs `sl:records:write`; THIS app's records in THIS collection only. A failing op
|
|
569
|
+
* rolls everything back; its error says which (`opIndex`) and why (`code`). With `idempotencyKey`, a
|
|
570
|
+
* repeat of the key (a scanner retrying after a dropped signal) returns the first results
|
|
571
|
+
* (`replayed: true`) instead of writing twice; keys are remembered for 30 days.
|
|
572
|
+
*/
|
|
573
|
+
transaction(ops: RecordTransactionOp[], opts?: {
|
|
574
|
+
idempotencyKey?: string;
|
|
575
|
+
}): Promise<{
|
|
576
|
+
results: any[];
|
|
577
|
+
replayed: boolean;
|
|
578
|
+
}>;
|
|
579
|
+
/**
|
|
580
|
+
* The app's HISTORY: append-only events kept in BigQuery (stock moved, a count taken, an order shipped),
|
|
581
|
+
* so the app's records hold current state and never grow with activity. `log` (`sl:history:write`)
|
|
582
|
+
* stores one event (it arrives within seconds; a failed delivery is retried, never lost); inside a
|
|
583
|
+
* transaction use a `log` op instead, so it commits with the change. `query` (`sl:history:read`) reads
|
|
584
|
+
* this app's events in this collection, newest first.
|
|
585
|
+
*/
|
|
586
|
+
history: {
|
|
587
|
+
log(type: string, data?: Record<string, any>, opts?: {
|
|
588
|
+
entityId?: string;
|
|
589
|
+
}): Promise<{
|
|
590
|
+
id: string;
|
|
591
|
+
}>;
|
|
592
|
+
query(params?: AppHistoryQuery): Promise<AppHistoryEvent[]>;
|
|
593
|
+
};
|
|
507
594
|
/** Atomic named counters for this app in this collection (`sl:data:write` / `sl:data:read`). */
|
|
508
595
|
counters: ServerFunctionCounters;
|
|
509
596
|
/** Tags (`sl:tags:read`): `resolve(id)` → the real tag in THIS collection, or `null` for a made-up id. */
|
|
@@ -55,8 +55,29 @@ export interface AggregateRequest {
|
|
|
55
55
|
expires_at?: {
|
|
56
56
|
lte?: string;
|
|
57
57
|
};
|
|
58
|
+
updated_at?: {
|
|
59
|
+
gte?: string;
|
|
60
|
+
lte?: string;
|
|
61
|
+
};
|
|
62
|
+
/** Records only: exact match on top-level data keys, one value or any of several. */
|
|
63
|
+
data?: Record<string, string | number | boolean | Array<string | number | boolean>>;
|
|
64
|
+
/** Records only: numeric comparisons on top-level data keys (numbers only; others never match). */
|
|
65
|
+
dataRange?: Record<string, {
|
|
66
|
+
gte?: number;
|
|
67
|
+
gt?: number;
|
|
68
|
+
lte?: number;
|
|
69
|
+
lt?: number;
|
|
70
|
+
}>;
|
|
58
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* Columns per resource; records also take top-level data keys as `'data.<key>'` (e.g. `'data.location'`),
|
|
74
|
+
* up to 5. Each group comes back with its key under the same name.
|
|
75
|
+
*/
|
|
59
76
|
groupBy?: string[];
|
|
77
|
+
/**
|
|
78
|
+
* `'count'`; records also take `'sum:<key>'`, `'avg:<key>'`, `'min:<key>'`, `'max:<key>'` over numbers in a
|
|
79
|
+
* top-level data key (non-numbers ignored) → `sum_<key>`, `avg_<key>`… in the result.
|
|
80
|
+
*/
|
|
60
81
|
metrics?: string[];
|
|
61
82
|
timeSeriesField?: string;
|
|
62
83
|
timeSeriesInterval?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
|
|
@@ -66,7 +87,7 @@ export interface AggregateRequest {
|
|
|
66
87
|
*/
|
|
67
88
|
export interface AggregateResponse {
|
|
68
89
|
groups?: ({
|
|
69
|
-
count
|
|
90
|
+
count?: number;
|
|
70
91
|
} & Record<string, unknown>)[];
|
|
71
92
|
timeSeries?: ({
|
|
72
93
|
bucket: string;
|
package/dist/types/claimSet.d.ts
CHANGED
|
@@ -361,6 +361,8 @@ export interface PartnerTagHit {
|
|
|
361
361
|
url: string | null;
|
|
362
362
|
};
|
|
363
363
|
range: ClaimSetRange | null;
|
|
364
|
+
/** The group the tag was added to, if any */
|
|
365
|
+
group: ClaimSetRange | null;
|
|
364
366
|
}
|
|
365
367
|
export interface CreatePartnerClaimSetRequest {
|
|
366
368
|
name: string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An app reporting an error it saw (errors.report): kept in the platform's one error log (CloudEvents + ECS
|
|
3
|
+
* rows in BigQuery), the same place server-side errors go. Say what it was about with entityType/entityId
|
|
4
|
+
* (a tag's chip UID, a product id, an order id), put routing facts in labels and the detail in context.
|
|
5
|
+
* Never include keys, passwords or tokens: the server drops any field whose name looks like one.
|
|
6
|
+
*/
|
|
7
|
+
export interface ClientErrorReport {
|
|
8
|
+
/** Which app: 'sidekick', 'studio', a site's app id. Default 'client'. */
|
|
9
|
+
app?: string;
|
|
10
|
+
appVersion?: string;
|
|
11
|
+
/** The collection it concerns, when there is one (owners see their collection's errors) */
|
|
12
|
+
collectionId?: string;
|
|
13
|
+
/** What was being done: 'tag.program', 'order.submit', … */
|
|
14
|
+
operation?: string;
|
|
15
|
+
severity?: 'debug' | 'info' | 'warning' | 'error' | 'critical';
|
|
16
|
+
/** A short stable code: 'TAG_LOST', 'TIMEOUT', … (this or message is required) */
|
|
17
|
+
errorCode?: string;
|
|
18
|
+
errorType?: string;
|
|
19
|
+
message?: string;
|
|
20
|
+
stack?: string;
|
|
21
|
+
/** What it was about: 'tag', 'product', 'order', … */
|
|
22
|
+
entityType?: string;
|
|
23
|
+
/** …and which one: a chip UID (normalised for 'tag'), a product id, … */
|
|
24
|
+
entityId?: string;
|
|
25
|
+
labels?: Record<string, unknown>;
|
|
26
|
+
context?: Record<string, unknown>;
|
|
27
|
+
/** An operation id to group by (a batch, a job) */
|
|
28
|
+
refId?: string;
|
|
29
|
+
correlationId?: string;
|
|
30
|
+
}
|
|
31
|
+
/** One kept error event, as the lookups return it (newest first). */
|
|
32
|
+
export interface ClientErrorEvent {
|
|
33
|
+
id: string;
|
|
34
|
+
time: string;
|
|
35
|
+
event_category: string;
|
|
36
|
+
event_action: string | null;
|
|
37
|
+
log_level: string;
|
|
38
|
+
error_code: string | null;
|
|
39
|
+
error_message: string | null;
|
|
40
|
+
service_name: string | null;
|
|
41
|
+
service_version: string | null;
|
|
42
|
+
collection_id: string | null;
|
|
43
|
+
user_id: string | null;
|
|
44
|
+
labels: Record<string, unknown> | null;
|
|
45
|
+
context: Record<string, unknown> | null;
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.17 | Generated: 2026-11-11T09:04:21.796Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -144,6 +144,7 @@ The Smartlinks SDK is organized into the following namespaces:
|
|
|
144
144
|
- **bookings** - Functions for bookings operations
|
|
145
145
|
- **config** - Functions for config operations
|
|
146
146
|
- **containers** - Functions for containers operations
|
|
147
|
+
- **errors** - Functions for errors operations
|
|
147
148
|
- **facets** - Functions for facets operations
|
|
148
149
|
- **functions** - Functions for functions operations
|
|
149
150
|
- **http** - Functions for http operations
|
|
@@ -2838,11 +2839,46 @@ interface ServerFunctionBookings {
|
|
|
2838
2839
|
}
|
|
2839
2840
|
```
|
|
2840
2841
|
|
|
2842
|
+
**AppHistoryEvent** (interface)
|
|
2843
|
+
```typescript
|
|
2844
|
+
interface AppHistoryEvent {
|
|
2845
|
+
id: string;
|
|
2846
|
+
at: string;
|
|
2847
|
+
type: string;
|
|
2848
|
+
entityId: string | null;
|
|
2849
|
+
data: Record<string, any>;
|
|
2850
|
+
actorType: 'user' | 'contact' | null;
|
|
2851
|
+
actorId: string | null;
|
|
2852
|
+
}
|
|
2853
|
+
```
|
|
2854
|
+
|
|
2855
|
+
**AppHistoryQuery** (interface)
|
|
2856
|
+
```typescript
|
|
2857
|
+
interface AppHistoryQuery {
|
|
2858
|
+
types?: string | string[];
|
|
2859
|
+
entityId?: string;
|
|
2860
|
+
data?: Record<string, string | number | boolean | Array<string | number | boolean>>;
|
|
2861
|
+
from?: string;
|
|
2862
|
+
to?: string;
|
|
2863
|
+
limit?: number;
|
|
2864
|
+
}
|
|
2865
|
+
```
|
|
2866
|
+
|
|
2841
2867
|
**ServerFunctionSl** (interface)
|
|
2842
2868
|
```typescript
|
|
2843
2869
|
interface ServerFunctionSl {
|
|
2844
2870
|
bookings: ServerFunctionBookings;
|
|
2845
|
-
appRecords: {
|
|
2871
|
+
appRecords: {
|
|
2872
|
+
claim(fields: Record<string, any>): Promise<AppRecordClaimResult>;
|
|
2873
|
+
adjust(recordId: string, by: Record<string, number>, opts?: { atLeast?: Record<string, number>; atMost?: Record<string, number> }): Promise<any>;
|
|
2874
|
+
aggregate(params?: import('./appObjects').AggregateRequest): Promise<import('./appObjects').AggregateResponse & Record<string, unknown>>;
|
|
2875
|
+
[method: string]: any;
|
|
2876
|
+
};
|
|
2877
|
+
transaction(ops: RecordTransactionOp[], opts?: { idempotencyKey?: string }): Promise<{ results: any[]; replayed: boolean }>;
|
|
2878
|
+
history: {
|
|
2879
|
+
log(type: string, data?: Record<string, any>, opts?: { entityId?: string }): Promise<{ id: string }>;
|
|
2880
|
+
query(params?: AppHistoryQuery): Promise<AppHistoryEvent[]>;
|
|
2881
|
+
};
|
|
2846
2882
|
counters: ServerFunctionCounters;
|
|
2847
2883
|
tags: { resolve(id: string): Promise<ResolvedTag | null> };
|
|
2848
2884
|
contacts: {
|
|
@@ -3298,6 +3334,8 @@ interface GetCollectionWidgetsOptions {
|
|
|
3298
3334
|
|
|
3299
3335
|
**AppFunctionAuthority** = `'caller' | 'collection'`
|
|
3300
3336
|
|
|
3337
|
+
**RecordTransactionOp** = ``
|
|
3338
|
+
|
|
3301
3339
|
### appObjects
|
|
3302
3340
|
|
|
3303
3341
|
**PaginatedResponse<T>** (interface)
|
|
@@ -3324,9 +3362,12 @@ interface AggregateRequest {
|
|
|
3324
3362
|
created_at?: { gte?: string; lte?: string }
|
|
3325
3363
|
closed_at?: '__notnull__' | { gte?: string; lte?: string } // cases only
|
|
3326
3364
|
expires_at?: { lte?: string } // records only
|
|
3365
|
+
updated_at?: { gte?: string; lte?: string } // records only
|
|
3366
|
+
data?: Record<string, string | number | boolean | Array<string | number | boolean>>
|
|
3367
|
+
dataRange?: Record<string, { gte?: number; gt?: number; lte?: number; lt?: number }>
|
|
3327
3368
|
}
|
|
3328
|
-
groupBy?: string[]
|
|
3329
|
-
metrics?: string[]
|
|
3369
|
+
groupBy?: string[]
|
|
3370
|
+
metrics?: string[]
|
|
3330
3371
|
timeSeriesField?: string // e.g. 'created_at'
|
|
3331
3372
|
timeSeriesInterval?: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'
|
|
3332
3373
|
}
|
|
@@ -3335,7 +3376,7 @@ interface AggregateRequest {
|
|
|
3335
3376
|
**AggregateResponse** (interface)
|
|
3336
3377
|
```typescript
|
|
3337
3378
|
interface AggregateResponse {
|
|
3338
|
-
groups?: ({ count
|
|
3379
|
+
groups?: ({ count?: number } & Record<string, unknown>)[]
|
|
3339
3380
|
timeSeries?: ({ bucket: string; count: number } & Record<string, unknown>)[]
|
|
3340
3381
|
count?: number
|
|
3341
3382
|
avg_close_time_seconds?: number
|
|
@@ -5966,6 +6007,7 @@ interface PartnerTagHit {
|
|
|
5966
6007
|
set: PartnerClaimSet | null
|
|
5967
6008
|
tag: ClaimSetRangeTag & { url: string | null }
|
|
5968
6009
|
range: ClaimSetRange | null
|
|
6010
|
+
group: ClaimSetRange | null
|
|
5969
6011
|
}
|
|
5970
6012
|
```
|
|
5971
6013
|
|
|
@@ -5993,6 +6035,48 @@ type ClaimSetScope = { collectionId: string }
|
|
|
5993
6035
|
|
|
5994
6036
|
**ClaimSetImportResult** = ``
|
|
5995
6037
|
|
|
6038
|
+
### clientLog
|
|
6039
|
+
|
|
6040
|
+
**ClientErrorReport** (interface)
|
|
6041
|
+
```typescript
|
|
6042
|
+
interface ClientErrorReport {
|
|
6043
|
+
app?: string
|
|
6044
|
+
appVersion?: string
|
|
6045
|
+
collectionId?: string
|
|
6046
|
+
operation?: string
|
|
6047
|
+
severity?: 'debug' | 'info' | 'warning' | 'error' | 'critical'
|
|
6048
|
+
errorCode?: string
|
|
6049
|
+
errorType?: string
|
|
6050
|
+
message?: string
|
|
6051
|
+
stack?: string
|
|
6052
|
+
entityType?: string
|
|
6053
|
+
entityId?: string
|
|
6054
|
+
labels?: Record<string, unknown>
|
|
6055
|
+
context?: Record<string, unknown>
|
|
6056
|
+
refId?: string
|
|
6057
|
+
correlationId?: string
|
|
6058
|
+
}
|
|
6059
|
+
```
|
|
6060
|
+
|
|
6061
|
+
**ClientErrorEvent** (interface)
|
|
6062
|
+
```typescript
|
|
6063
|
+
interface ClientErrorEvent {
|
|
6064
|
+
id: string
|
|
6065
|
+
time: string
|
|
6066
|
+
event_category: string
|
|
6067
|
+
event_action: string | null
|
|
6068
|
+
log_level: string
|
|
6069
|
+
error_code: string | null
|
|
6070
|
+
error_message: string | null
|
|
6071
|
+
service_name: string | null
|
|
6072
|
+
service_version: string | null
|
|
6073
|
+
collection_id: string | null
|
|
6074
|
+
user_id: string | null
|
|
6075
|
+
labels: Record<string, unknown> | null
|
|
6076
|
+
context: Record<string, unknown> | null
|
|
6077
|
+
}
|
|
6078
|
+
```
|
|
6079
|
+
|
|
5996
6080
|
### collection
|
|
5997
6081
|
|
|
5998
6082
|
**Collection** (interface)
|
|
@@ -12830,6 +12914,10 @@ Update a crate for a collection (admin only). ```typescript const updated = awai
|
|
|
12830
12914
|
crateId: string) → `Promise<DeleteCrateResponse>`
|
|
12831
12915
|
Delete a crate for a collection (admin only). This performs a soft delete. ```typescript await crate.remove('coll_123', 'crate_abc123') ```
|
|
12832
12916
|
|
|
12917
|
+
### errors
|
|
12918
|
+
|
|
12919
|
+
**report**(event: ClientErrorReport) → `Promise<`
|
|
12920
|
+
|
|
12833
12921
|
### facets
|
|
12834
12922
|
|
|
12835
12923
|
**list**(collectionId: string,
|
|
@@ -13462,6 +13550,9 @@ A new claim set for the partner, on one of its tap domains (owners and admins).
|
|
|
13462
13550
|
**lookupTags**(partnerId: string, query: { uid?: string; code?: string }) → `Promise<PartnerTagHit[]>`
|
|
13463
13551
|
Find a tag across the partner's sets by its chip UID (8 or 14 hex) or its code (printed or bare). Never returns keys.
|
|
13464
13552
|
|
|
13553
|
+
**tagAttempts**(partnerId: string, uid: string) → `Promise<ClientErrorEvent[]>`
|
|
13554
|
+
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. For a chip in the partner's sets, or attempts that named one of them.
|
|
13555
|
+
|
|
13465
13556
|
### podcast
|
|
13466
13557
|
|
|
13467
13558
|
**generate**(collectionId: string,
|
package/docs/app-objects.md
CHANGED
|
@@ -807,6 +807,29 @@ await app.records.upsert(collectionId, appId, {
|
|
|
807
807
|
// customId is stored for later reverse-lookup via ?customId=&sourceSystem=
|
|
808
808
|
```
|
|
809
809
|
|
|
810
|
+
### Totals over data (sum, average, min, max)
|
|
811
|
+
|
|
812
|
+
Records also total the numbers in their `data`, grouped by top-level data keys, with data filters and
|
|
813
|
+
numeric ranges. Stock per location, forecast per SKU:
|
|
814
|
+
|
|
815
|
+
```typescript
|
|
816
|
+
const stock = await app.records.aggregate(collectionId, appId, {
|
|
817
|
+
groupBy: ['data.location'], // up to 5: columns or 'data.<key>'
|
|
818
|
+
metrics: ['count', 'sum:qty', 'min:qty'], // count; sum: / avg: / min: / max: + a data key
|
|
819
|
+
filters: {
|
|
820
|
+
record_type: 'bin',
|
|
821
|
+
data: { warehouse: ['north', 'south'] }, // exact match, one value or any of several
|
|
822
|
+
dataRange: { qty: { gt: 0 } }, // gte / gt / lte / lt, numbers only
|
|
823
|
+
updated_at: { gte: '2026-11-01T00:00:00Z' },
|
|
824
|
+
},
|
|
825
|
+
}, true)
|
|
826
|
+
// stock.groups → [{ 'data.location': 'A1', count: 12, sum_qty: 340, min_qty: 2 }, …]
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
Values that aren't numbers are left out of sums and averages. To CHANGE numbers safely (stock moves,
|
|
830
|
+
counts), use a server function: `ctx.sl.appRecords.adjust` and `ctx.sl.transaction`
|
|
831
|
+
([server-functions.md](server-functions.md)).
|
|
832
|
+
|
|
810
833
|
### Counts by Record Type
|
|
811
834
|
|
|
812
835
|
`aggregate()` returns counts grouped by `record_type` in a single round-trip — no separate endpoint needed:
|