@sasjs/cli 6.25.0 → 6.26.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.
@@ -3,29 +3,40 @@
3
3
  *
4
4
  * The service worker hosts the runtime but may be terminated between requests,
5
5
  * so it holds no durable state. On boot this script reads the persisted drive
6
- * and hands it to the worker; after every service run the worker sends back the
7
- * files it changed and they are written here.
6
+ * and identity and hands them to the worker; after every service run the worker
7
+ * sends back the files it changed and the identity store's snapshot when a route
8
+ * changed it, and both are written here.
8
9
  */
9
10
  ;(function () {
10
11
  const DB_NAME = 'sasjs-mock-host'
11
- const STORE = 'files'
12
+ const FILES = 'files'
13
+ /**
14
+ * The identity the visitor last had: the fixture the worker's store reported
15
+ * and the account the visitor chose to view the app as. One row, because the
16
+ * two are written together - a subject without the fixture it names would be a
17
+ * boot signed in as an account the store no longer holds.
18
+ */
19
+ const IDENTITY = 'identity'
20
+ const IDENTITY_KEY = 'current'
12
21
 
13
22
  const openDb = () =>
14
23
  new Promise((resolve, reject) => {
15
- const req = indexedDB.open(DB_NAME, 1)
24
+ const req = indexedDB.open(DB_NAME, 2)
16
25
  req.onupgradeneeded = () => {
17
26
  const db = req.result
18
- if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE)
27
+ if (!db.objectStoreNames.contains(FILES)) db.createObjectStore(FILES)
28
+ if (!db.objectStoreNames.contains(IDENTITY))
29
+ db.createObjectStore(IDENTITY)
19
30
  }
20
31
  req.onsuccess = () => resolve(req.result)
21
32
  req.onerror = () => reject(req.error)
22
33
  })
23
34
 
24
- const withStore = async (mode, run) => {
35
+ const withStore = async (mode, run, name = FILES) => {
25
36
  const db = await openDb()
26
37
  return new Promise((resolve, reject) => {
27
- const tx = db.transaction(STORE, mode)
28
- const store = tx.objectStore(STORE)
38
+ const tx = db.transaction(name, mode)
39
+ const store = tx.objectStore(name)
29
40
  let result
30
41
  try {
31
42
  result = run(store)
@@ -44,15 +55,15 @@
44
55
  try {
45
56
  const db = await openDb()
46
57
  const keysReq = db
47
- .transaction(STORE, 'readonly')
48
- .objectStore(STORE)
58
+ .transaction(FILES, 'readonly')
59
+ .objectStore(FILES)
49
60
  .getAllKeys()
50
61
  keysReq.onsuccess = () => {
51
62
  const keys = keysReq.result
52
63
  if (!keys.length) return resolve({})
53
64
  const valsReq = db
54
- .transaction(STORE, 'readonly')
55
- .objectStore(STORE)
65
+ .transaction(FILES, 'readonly')
66
+ .objectStore(FILES)
56
67
  .getAll()
57
68
  valsReq.onsuccess = () => {
58
69
  const files = {}
@@ -89,6 +100,36 @@
89
100
 
90
101
  const clearAll = () => withStore('readwrite', (store) => store.clear())
91
102
 
103
+ /**
104
+ * The stored identity, `null` when there is none. A row whose shape is not the
105
+ * one this script writes is ignored rather than handed on: the worker refuses a
106
+ * fixture it cannot read by name, and re-sending a value that is not a row at
107
+ * all would blame the fixture for a corrupt database.
108
+ */
109
+ const readIdentity = () =>
110
+ new Promise(async (resolve, reject) => {
111
+ try {
112
+ const db = await openDb()
113
+ const req = db
114
+ .transaction(IDENTITY, 'readonly')
115
+ .objectStore(IDENTITY)
116
+ .get(IDENTITY_KEY)
117
+ req.onsuccess = () => {
118
+ const row = req.result
119
+ resolve(row && typeof row === 'object' && row.fixture ? row : null)
120
+ }
121
+ req.onerror = () => reject(req.error)
122
+ } catch (e) {
123
+ reject(e)
124
+ }
125
+ })
126
+
127
+ const writeIdentity = (row) =>
128
+ withStore('readwrite', (store) => store.put(row, IDENTITY_KEY), IDENTITY)
129
+
130
+ const clearIdentity = () =>
131
+ withStore('readwrite', (store) => store.delete(IDENTITY_KEY), IDENTITY)
132
+
92
133
  /**
93
134
  * The build this site is, as the page can see it: mock-config.json carries the
94
135
  * stamp the build wrote over its host code and the seed it lays out. Read here
@@ -191,13 +232,29 @@
191
232
  })
192
233
  })
193
234
 
194
- // Persist whatever the worker reports as changed.
235
+ // Persist whatever the worker reports as changed: drive files, and the
236
+ // identity store's snapshot when a route created, changed or removed anything.
195
237
  navigator.serviceWorker.addEventListener('message', (ev) => {
196
238
  const data = ev.data || {}
197
- if (data.type === 'sasjs-persist')
239
+ if (data.type === 'sasjs-persist') {
198
240
  writeFiles(data.files).catch((e) =>
199
241
  console.error('[sasjs-mock] persist failed', e)
200
242
  )
243
+ if (data.identity)
244
+ readIdentity()
245
+ .then((row) =>
246
+ // `undefined`, not null: no stored subject means the fixture's own
247
+ // administrator, which is not the same as the anonymous caller a
248
+ // null would name
249
+ writeIdentity({
250
+ fixture: data.identity,
251
+ subject: row ? row.subject : undefined
252
+ })
253
+ )
254
+ .catch((e) =>
255
+ console.error('[sasjs-mock] identity persist failed', e)
256
+ )
257
+ }
201
258
  if (data.type === 'sasjs-host-error')
202
259
  console.error('[sasjs-mock] ' + data.path + ': ' + data.message)
203
260
  })
@@ -206,7 +263,52 @@
206
263
  readAll,
207
264
  writeFiles,
208
265
  clear: clearAll,
266
+ identity: readIdentity,
209
267
  snapshot: () => askWorker({ type: 'sasjs-snapshot' }, 'sasjs-snapshot'),
268
+ /**
269
+ * Views the app as another account: `as('sasjsuser')` for a restricted user,
270
+ * `as()` or `as('admin')` to return to the fixture's administrator.
271
+ *
272
+ * The worker issues a token pair for the account and answers as it from then
273
+ * on; the subject and the fixture are stored here so a reload stays that
274
+ * account, and the app is restarted so every screen redraws its menus and
275
+ * refusals. The pair is returned for a caller who wants to send it as a
276
+ * bearer token - nothing signs it, and no password is checked.
277
+ */
278
+ as: async (username) => {
279
+ const answer = await askWorker(
280
+ { type: 'sasjs-as', username },
281
+ 'sasjs-as-ack'
282
+ )
283
+ if (answer.error) throw new Error(answer.error)
284
+
285
+ try {
286
+ await writeIdentity({
287
+ fixture: answer.identity,
288
+ subject: answer.username
289
+ })
290
+ } catch (e) {
291
+ // the subject still applies to this run; it just will not outlive it
292
+ console.error('[sasjs-mock] could not store the subject: ' + e)
293
+ }
294
+
295
+ const pair = {
296
+ username: answer.username,
297
+ accessToken: answer.accessToken,
298
+ refreshToken: answer.refreshToken
299
+ }
300
+
301
+ // a task, so the caller's promise resolves with the pair first
302
+ setTimeout(() => {
303
+ try {
304
+ window.location.reload()
305
+ } catch (e) {
306
+ /* a page that cannot reload keeps the subject it already has */
307
+ }
308
+ }, 0)
309
+
310
+ return pair
311
+ },
210
312
  /**
211
313
  * The drive total in bytes, measured by the worker: it owns the stored
212
314
  * representation, so the figure stays the one `statSync` reports rather than
@@ -249,20 +351,62 @@
249
351
  return Object.keys(files).length
250
352
  },
251
353
  reset: async () => {
252
- await askWorker({ type: 'sasjs-reset' }, 'sasjs-reset-ack')
354
+ const answer = await askWorker({ type: 'sasjs-reset' }, 'sasjs-reset-ack')
355
+ if (answer.error) throw new Error(answer.error)
253
356
  await clearAll()
357
+ await clearIdentity()
254
358
  return true
255
359
  }
256
360
  }
257
361
 
362
+ /**
363
+ * Boots the worker with what is stored, and recovers when the stored identity
364
+ * is one this build cannot read.
365
+ *
366
+ * A fixture outlives the build that wrote it, exactly as the drive does, and
367
+ * the worker refuses one whose format stamp it does not speak - by name, and
368
+ * with `errorKind: 'identity'`. Only that answer drops the stored row: the
369
+ * row holds the accounts the visitor created as well as the fixture, so a
370
+ * host-side failure (a drive that could not be read) leaves it alone.
371
+ */
372
+ const boot = async (files, stored) => {
373
+ const loading = Object.keys(files).length > 0
374
+ const type = loading ? 'sasjs-load' : 'sasjs-seed'
375
+ const ack = loading ? 'sasjs-load-ack' : 'sasjs-seed-ack'
376
+
377
+ const answer = await askWorker(
378
+ {
379
+ type,
380
+ files,
381
+ identity: stored ? stored.fixture : undefined,
382
+ subject: stored ? stored.subject : undefined
383
+ },
384
+ ack
385
+ )
386
+
387
+ if (answer.error && stored && answer.errorKind === 'identity') {
388
+ console.info('[sasjs-mock] ' + answer.error)
389
+ try {
390
+ await clearIdentity()
391
+ } catch (e) {
392
+ console.error('[sasjs-mock] could not clear the stored identity: ' + e)
393
+ }
394
+ return await askWorker({ type, files }, ack)
395
+ }
396
+
397
+ return answer
398
+ }
399
+
258
400
  window.__sasjsMockReady = (async () => {
259
401
  await navigator.serviceWorker.ready
260
402
  const controller = await waitForController()
261
403
  if (!controller) return { error: 'no service worker controller' }
262
404
 
263
405
  let files = {}
406
+ let stored = null
264
407
  try {
265
408
  files = await readAll()
409
+ stored = await readIdentity()
266
410
  } catch (e) {
267
411
  console.error('[sasjs-mock] could not read storage: ' + e)
268
412
  }
@@ -287,23 +431,18 @@
287
431
  )
288
432
  try {
289
433
  await clearAll()
434
+ // the identity is part of what this build ships, so a visitor who kept
435
+ // the previous build's fixture would never see a changed
436
+ // `mock-identity.json`: it goes with the drive, as it does on reset
437
+ await clearIdentity()
290
438
  } catch (e) {
291
439
  console.error('[sasjs-mock] could not clear storage: ' + e)
292
440
  }
293
441
  files = {}
442
+ stored = null
294
443
  }
295
444
 
296
- if (Object.keys(files).length) {
297
- window.__sasjsMockBoot = await askWorker(
298
- { type: 'sasjs-load', files },
299
- 'sasjs-load-ack'
300
- )
301
- } else {
302
- window.__sasjsMockBoot = await askWorker(
303
- { type: 'sasjs-seed' },
304
- 'sasjs-seed-ack'
305
- )
306
- }
445
+ window.__sasjsMockBoot = await boot(files, stored)
307
446
  if (window.__sasjsMockBoot && !window.__sasjsMockBoot.error)
308
447
  keepBuildStamp(stamp)
309
448
  return window.__sasjsMockBoot