@uniweb/api 0.5.0 → 0.5.1
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 +3 -1
- package/package.json +1 -1
- package/src/mock/store.js +33 -8
package/README.md
CHANGED
|
@@ -206,7 +206,9 @@ who may create entries, which sections are insert-only, the fields a section tak
|
|
|
206
206
|
and it is as strict as the backend about requests, so a mistake fails here rather
|
|
207
207
|
than in front of a user. Members read and write their own entities and nothing of each
|
|
208
208
|
other's; the operator reads and writes everything (`memberFloor: 'read'` in the seed
|
|
209
|
-
models a service set up to let members read each other's
|
|
209
|
+
models a service set up to let members read each other's — or per Model,
|
|
210
|
+
`memberFloor: { '@acme/course': 'read' }`, so the operator's content is shared while
|
|
211
|
+
each member's own records stay private). A new sign-up must be
|
|
210
212
|
verified: the link is in `mock.outbox` (the standalone server prints it). State is in
|
|
211
213
|
memory: restart to reset.
|
|
212
214
|
|
package/package.json
CHANGED
package/src/mock/store.js
CHANGED
|
@@ -85,6 +85,9 @@ function canonical(value) {
|
|
|
85
85
|
const isMulti = (section) => section?.multiple === true || section?.kind === 'multi'
|
|
86
86
|
const shortName = (model) => String(model).split('/').pop() || String(model)
|
|
87
87
|
|
|
88
|
+
/** A `{ locale: value }` map — a plain object, never an array (a localized list's value). */
|
|
89
|
+
const isLocaleMap = (value) => value != null && typeof value === 'object' && !Array.isArray(value)
|
|
90
|
+
|
|
88
91
|
export class MockStore {
|
|
89
92
|
/**
|
|
90
93
|
* @param {object} seed
|
|
@@ -99,8 +102,11 @@ export class MockStore {
|
|
|
99
102
|
* seed's items and `append_only` use.
|
|
100
103
|
* @param {object[]} [seed.entities] - `{ uuid?, model, owner?, items?: [{ section, data, parent? }] }`.
|
|
101
104
|
* `uuid` must be a UUID. (`data`, the older spelling, becomes the brief section's item.)
|
|
102
|
-
* @param {'read'|'edit'} [seed.memberFloor] - what membership
|
|
103
|
-
* ANOTHER member's entities. Absent — the default — nothing: each
|
|
105
|
+
* @param {'read'|'edit'|Object<string,'read'|'edit'>} [seed.memberFloor] - what membership
|
|
106
|
+
* lets one member do to ANOTHER member's entities. Absent — the default — nothing: each
|
|
107
|
+
* member's are private. A string applies to every Model; a map `{ '@scope/name': 'read' }`
|
|
108
|
+
* applies per Model, so a service can open the operator's content (courses, lessons) to
|
|
109
|
+
* its members while their own records (progress, submissions) stay theirs alone.
|
|
104
110
|
* @param {object} [options]
|
|
105
111
|
* @param {string} [options.signedInAs] - start with this account already signed in.
|
|
106
112
|
*/
|
|
@@ -111,10 +117,16 @@ export class MockStore {
|
|
|
111
117
|
for (const a of seed.accounts || []) this.addAccount(a, { verified: true })
|
|
112
118
|
|
|
113
119
|
this.schemas = seed.schemas || {}
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
const floors = seed.memberFloor != null && typeof seed.memberFloor === 'object' ? Object.values(seed.memberFloor) : [seed.memberFloor]
|
|
121
|
+
for (const floor of floors) {
|
|
122
|
+
if (floor != null && !['read', 'edit'].includes(floor)) {
|
|
123
|
+
throw new Error(`[uniweb/api mock] memberFloor is 'read' or 'edit', or absent — per Model or for all — got ${JSON.stringify(seed.memberFloor)}`)
|
|
124
|
+
}
|
|
116
125
|
}
|
|
117
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* What membership lets one member do to another's entities: `null` (nothing), `read`
|
|
128
|
+
* or `edit` — for every Model, or a map of it per Model.
|
|
129
|
+
*/
|
|
118
130
|
this.memberFloor = seed.memberFloor ?? null
|
|
119
131
|
this.models = new Map()
|
|
120
132
|
const itemSections = new Map()
|
|
@@ -429,15 +441,22 @@ export class MockStore {
|
|
|
429
441
|
return siblings[siblings.length - 1].order_number + GAP
|
|
430
442
|
}
|
|
431
443
|
|
|
444
|
+
/** The floor that applies to this entity's Model: `null`, `'read'` or `'edit'`. */
|
|
445
|
+
floorFor(entity) {
|
|
446
|
+
const floor = this.memberFloor
|
|
447
|
+
if (floor == null || typeof floor === 'string') return floor
|
|
448
|
+
return floor[entity.model] ?? null
|
|
449
|
+
}
|
|
450
|
+
|
|
432
451
|
/** The operator, the owner — or any member, when the service lets members read each other's. */
|
|
433
452
|
mayRead(entity) {
|
|
434
453
|
const me = this.account
|
|
435
|
-
return !!me && (me.operator || entity.owner_id === me.id || this.
|
|
454
|
+
return !!me && (me.operator || entity.owner_id === me.id || this.floorFor(entity) != null)
|
|
436
455
|
}
|
|
437
456
|
|
|
438
457
|
mayEdit(entity) {
|
|
439
458
|
const me = this.account
|
|
440
|
-
return !!me && (me.operator || entity.owner_id === me.id || this.
|
|
459
|
+
return !!me && (me.operator || entity.owner_id === me.id || this.floorFor(entity) === 'edit')
|
|
441
460
|
}
|
|
442
461
|
|
|
443
462
|
/**
|
|
@@ -466,12 +485,18 @@ export class MockStore {
|
|
|
466
485
|
* A localized field — declared `localized: true` — answered in the first listed
|
|
467
486
|
* locale it has, and omitted when it has none: the backend has no fallback of
|
|
468
487
|
* its own. Without a locale, the whole `{ locale: value }` map.
|
|
488
|
+
*
|
|
489
|
+
* ⛔ Only a plain object is a locale map. A localized LIST (`multiple: true`) holds
|
|
490
|
+
* an array, and an array is a value, not a map of them: read as a map it has no
|
|
491
|
+
* `en`, and the field was DELETED from every read that carried `?locale=` — which
|
|
492
|
+
* the client sends on every browser read. Measured 2026-09-22 on a course brief
|
|
493
|
+
* whose `outcomes` vanished, and on quiz questions whose `options` did.
|
|
469
494
|
*/
|
|
470
495
|
project(data, section, locales) {
|
|
471
496
|
if (!locales?.length || !section?.fields || !data || typeof data !== 'object') return data
|
|
472
497
|
const out = { ...data }
|
|
473
498
|
for (const [key, field] of Object.entries(section.fields)) {
|
|
474
|
-
if (!field?.localized || !out[key]
|
|
499
|
+
if (!field?.localized || !isLocaleMap(out[key])) continue
|
|
475
500
|
const hit = locales.find((l) => out[key][l] != null)
|
|
476
501
|
if (hit) out[key] = out[key][hit]
|
|
477
502
|
else delete out[key]
|