@awesomate/sdk 0.8.0 → 0.10.0

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/README.md CHANGED
@@ -146,6 +146,11 @@ and takes a fresh copy when it does. Up to 200 rows a list and 20 lists a connec
146
146
  `{ onRows, onError }` to hear about a list the server refused (a column that does not exist).
147
147
  Outside a browser, give it a WebSocket: `createAppClient({ publishableKey, WebSocket })`.
148
148
 
149
+ While a list is open in a browser, the connection also tells the hub whether the tab is in front.
150
+ If the account switched on reply emails, someone looking at the app isn't emailed about a reply,
151
+ but a tab left open in the background doesn't count as looking, and closing the last one counts as
152
+ leaving straight away.
153
+
149
154
  ### Staff find a customer
150
155
 
151
156
  People in an app read no contacts but their own, so by default staff cannot put a job under a
@@ -160,6 +165,22 @@ await app.write('job', { title: 'Gutters' }, { links: { customer: jo.id } });
160
165
 
161
166
  Name, email and phone only, and nothing the owner marked sensitive. Anyone else is refused.
162
167
 
168
+ An attribute can also be visible to some roles only (`awesomate_crm_kinds`, `set_visibility`, for
169
+ example a cost only `staff` see). For everyone else it comes back `null` and a write that sets it
170
+ is refused, whatever the app asks for.
171
+
172
+ Some things a customer should do without being able to edit the record: accept their quote, say.
173
+ Save it as a write recipe and open it to their role (`awesomate_crm_recipes`, `run_by`, Pro and
174
+ above), then:
175
+
176
+ ```ts
177
+ await app.call('accept_quote', { job: job.id });
178
+ ```
179
+
180
+ The database lets them do exactly the recipe's steps: only its params take their values, every
181
+ other value is fixed, and an existing record must be one they can already read. A recipe not
182
+ opened to their role is "not found".
183
+
163
184
  The rules are kept in your own database, which applies them to every read and write, so a mistake
164
185
  in the app cannot show anyone more than their role allows. A disabled person is refused on their
165
186
  next call. Sessions are kept in `localStorage`; in a Capacitor app pass `storage` (Preferences) and
package/dist/index.d.ts CHANGED
@@ -492,6 +492,13 @@ export declare class AwesomateAppClient {
492
492
  /** Create a record (returns its id) or change one with options.id, inside the kind's write rule for this user. */
493
493
  write<K extends KindName>(kind: K, data: Partial<RowOf<K>> | Record<string, unknown>, options?: WriteOptions): Promise<string>;
494
494
  archive<K extends KindName>(kind: K, id: string): Promise<void>;
495
+ /**
496
+ * Run a recipe the account opened to this person's role: "accept this quote", something their
497
+ * write rule would not let them do by hand. Every step commits or none does; a recipe they may
498
+ * not run reads as not found. Never retried once sent: a write that may have landed is not safe
499
+ * to send twice (a refused sign-in is, and is retried after a refresh like every data call).
500
+ */
501
+ call<R extends RecipeName>(recipe: R, args?: ArgsOf<R>): Promise<RecipeResult>;
495
502
  /**
496
503
  * Customers by name or email (two characters or more), or by id, so staff can find one and put
497
504
  * a record under them. Only for a role the account lets look customers up in this app; anyone
package/dist/index.js CHANGED
@@ -514,6 +514,15 @@ export class AwesomateAppClient {
514
514
  async archive(kind, id) {
515
515
  await this.data('POST', '/call', { fn: 'archive_record', args: { kind, id } });
516
516
  }
517
+ /**
518
+ * Run a recipe the account opened to this person's role: "accept this quote", something their
519
+ * write rule would not let them do by hand. Every step commits or none does; a recipe they may
520
+ * not run reads as not found. Never retried once sent: a write that may have landed is not safe
521
+ * to send twice (a refused sign-in is, and is retried after a refresh like every data call).
522
+ */
523
+ async call(recipe, args = {}) {
524
+ return (await this.data('POST', '/call', { fn: recipe, args })).result;
525
+ }
517
526
  /**
518
527
  * Customers by name or email (two characters or more), or by id, so staff can find one and put
519
528
  * a record under them. Only for a role the account lets look customers up in this app; anyone
@@ -544,6 +553,10 @@ function unref(t) {
544
553
  t.unref?.();
545
554
  return t;
546
555
  }
556
+ /** The page's document, when there is one: the part live() reads to say whether the tab is in front. */
557
+ function pageDocument() {
558
+ return globalThis.document;
559
+ }
547
560
  /** The one WebSocket behind live(). */
548
561
  class LiveSocket {
549
562
  o;
@@ -560,6 +573,15 @@ class LiveSocket {
560
573
  connecting = false;
561
574
  constructor(o) {
562
575
  this.o = o;
576
+ // Reply emails wait only while one of the person's tabs is in front, so the hub hears when this
577
+ // one goes to the back or comes forward. Outside a browser there is no tab, and nothing is sent.
578
+ pageDocument()?.addEventListener?.('visibilitychange', () => this.sendPresence());
579
+ }
580
+ /** Whether this tab is in front, to the hub (from 0.10.0; an older hub ignores the frame). */
581
+ sendPresence() {
582
+ const d = pageDocument();
583
+ if (this.ready && typeof d?.visibilityState === 'string')
584
+ this.send({ type: 'presence', visible: d.visibilityState === 'visible' });
563
585
  }
564
586
  add(spec, handlers) {
565
587
  const id = `l${++this.seq}`;
@@ -623,6 +645,7 @@ class LiveSocket {
623
645
  this.attempt = 0;
624
646
  for (const [id, e] of this.entries)
625
647
  this.send({ type: 'sub', id, spec: e.spec });
648
+ this.sendPresence();
626
649
  this.pinger = unref(setInterval(() => this.send({ type: 'ping' }), 25_000));
627
650
  return;
628
651
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awesomate/sdk",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Your own Awesomate data from Node and the browser: query contacts and app data with generated types, and sign your app's own users in",
5
5
  "license": "MIT",
6
6
  "type": "module",