@cube-nl/dmp-sdk 1.1.0-rc.1 → 1.1.0-rc.3
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 +115 -27
- package/dist/app.d.ts +7 -4
- package/dist/app.js +89 -33
- package/dist/browser.js +8 -0
- package/dist/client.js +5 -1
- package/dist/cube-sdk.js +32 -22
- package/dist/cube-ui.manifest.json +12 -5
- package/dist/preview.d.ts +2 -0
- package/dist/preview.js +21 -6
- package/dist/size.json +3 -3
- package/dist/trial.d.ts +32 -0
- package/dist/trial.js +55 -0
- package/dist/ui/aggregate-block.js +2 -1
- package/dist/ui/attributes.js +1 -1
- package/dist/ui/author-error.d.ts +3 -0
- package/dist/ui/author-error.js +3 -0
- package/dist/ui/block.d.ts +8 -2
- package/dist/ui/block.js +14 -5
- package/dist/ui/blocks/app-shell.js +1 -1
- package/dist/ui/blocks/board.d.ts +1 -1
- package/dist/ui/blocks/board.js +129 -16
- package/dist/ui/blocks/calc.js +35 -12
- package/dist/ui/blocks/kpi.js +2 -1
- package/dist/ui/blocks/table.js +82 -23
- package/dist/ui/dom.js +2 -1
- package/dist/ui/format.js +3 -2
- package/dist/ui/manifest.js +14 -3
- package/dist/ui/query.js +7 -6
- package/dist/ui/texts.d.ts +12 -0
- package/dist/ui/texts.js +16 -0
- package/dist/ui/trial-banner.d.ts +27 -0
- package/dist/ui/trial-banner.js +199 -0
- package/dist/ui/write.d.ts +2 -2
- package/dist/ui/write.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wireframe.js +53 -16
- package/package.json +5 -2
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { clear, h } from './dom.js';
|
|
2
|
+
import { BASE_CSS } from './styles.js';
|
|
3
|
+
import { NL } from './texts.js';
|
|
4
|
+
/** Spec section 7: every 5 s while the page is visible, every 60 s while it is hidden. */
|
|
5
|
+
export const POLL_VISIBLE_MS = 5000;
|
|
6
|
+
export const POLL_HIDDEN_MS = 60000;
|
|
7
|
+
/** A 429's numeric Retry-After is honoured up to this long, and never past the expiry. */
|
|
8
|
+
export const RETRY_AFTER_CAP_MS = 60000;
|
|
9
|
+
/** A status request that hangs this long is given up as a network error, so the polling goes on. */
|
|
10
|
+
export const STATUS_TIMEOUT_MS = 10000;
|
|
11
|
+
const CSS = `
|
|
12
|
+
.banner{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;padding:.5rem 1rem;
|
|
13
|
+
border-bottom:1px solid var(--cube-color-border);border-left:4px solid var(--cube-color-warning);
|
|
14
|
+
background:var(--cube-color-surface);font-size:.875rem}
|
|
15
|
+
.banner p{margin:0;flex:1 1 16rem}
|
|
16
|
+
.banner[data-state=expired],.banner[data-state=closed]{border-left-color:var(--cube-color-danger)}
|
|
17
|
+
`;
|
|
18
|
+
/**
|
|
19
|
+
* Puts `cube-trial-banner` at the top of `<body>` and polls the trial's status: it says what the page is, ends when
|
|
20
|
+
* the trial does, and follows a newer version (by itself only when that one reads nothing more).
|
|
21
|
+
* Returns a function that stops the polling.
|
|
22
|
+
*/
|
|
23
|
+
export function showTrialBanner(scope, config) {
|
|
24
|
+
const doc = scope.document;
|
|
25
|
+
const now = scope.now ?? Date.now;
|
|
26
|
+
const expiresAt = Date.parse(config.expiresAt);
|
|
27
|
+
const known = Number.isFinite(expiresAt);
|
|
28
|
+
const time = known
|
|
29
|
+
? new Intl.DateTimeFormat('nl-NL', { hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }).format(new Date(expiresAt))
|
|
30
|
+
: null;
|
|
31
|
+
const host = doc.createElement('cube-trial-banner');
|
|
32
|
+
const root = host.attachShadow({ mode: 'open' });
|
|
33
|
+
const sheets = [BASE_CSS, CSS].map((css) => {
|
|
34
|
+
const sheet = new scope.CSSStyleSheet();
|
|
35
|
+
sheet.replaceSync(css);
|
|
36
|
+
return sheet;
|
|
37
|
+
});
|
|
38
|
+
const message = h(doc, 'p', {}, NL.trial.banner(time, config.leest));
|
|
39
|
+
const region = h(doc, 'div', { class: 'banner', role: 'status', 'aria-live': 'polite' }, message);
|
|
40
|
+
let timer = null;
|
|
41
|
+
let ended = false;
|
|
42
|
+
let inFlight = false;
|
|
43
|
+
let abort = null;
|
|
44
|
+
/** Until when a 429 asked to wait; 0 when no back-off holds. */
|
|
45
|
+
let backOffUntil = 0;
|
|
46
|
+
root.adoptedStyleSheets = sheets;
|
|
47
|
+
root.appendChild(region);
|
|
48
|
+
const insert = () => {
|
|
49
|
+
doc.body.insertBefore(host, doc.body.firstChild);
|
|
50
|
+
};
|
|
51
|
+
const visible = () => doc.visibilityState !== 'hidden';
|
|
52
|
+
const stop = () => {
|
|
53
|
+
ended = true;
|
|
54
|
+
abort?.abort();
|
|
55
|
+
if (timer !== null)
|
|
56
|
+
scope.clearTimeout(timer);
|
|
57
|
+
timer = null;
|
|
58
|
+
doc.removeEventListener('visibilitychange', onVisibility);
|
|
59
|
+
};
|
|
60
|
+
const end = (state, action) => {
|
|
61
|
+
stop();
|
|
62
|
+
// The app may have replaced <body>'s children: without the banner the end state would go unseen.
|
|
63
|
+
if (!host.isConnected && doc.body !== null)
|
|
64
|
+
insert();
|
|
65
|
+
region.setAttribute('data-state', state);
|
|
66
|
+
clear(message);
|
|
67
|
+
message.appendChild(doc.createTextNode(NL.trial[state]));
|
|
68
|
+
if (action !== undefined) {
|
|
69
|
+
region.appendChild(h(doc, 'button', { type: 'button', onclick: action[1] }, action[0]));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const schedule = (beat = visible() ? POLL_VISIBLE_MS : POLL_HIDDEN_MS) => {
|
|
73
|
+
if (ended)
|
|
74
|
+
return;
|
|
75
|
+
if (timer !== null)
|
|
76
|
+
scope.clearTimeout(timer);
|
|
77
|
+
// Capped again: a wall clock that jumps back must not stretch the back-off.
|
|
78
|
+
const wait = Math.max(beat, Math.min(RETRY_AFTER_CAP_MS, backOffUntil - now()));
|
|
79
|
+
timer = scope.setTimeout(tick, known ? Math.max(0, Math.min(wait, expiresAt - now())) : wait);
|
|
80
|
+
};
|
|
81
|
+
const tick = async () => {
|
|
82
|
+
timer = null;
|
|
83
|
+
if (ended)
|
|
84
|
+
return;
|
|
85
|
+
// The hour is over: no request needed to know it.
|
|
86
|
+
if (known && now() >= expiresAt) {
|
|
87
|
+
end('expired');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// A back-off governs only the request it delays; the answer to this one sets the next.
|
|
91
|
+
backOffUntil = 0;
|
|
92
|
+
inFlight = true;
|
|
93
|
+
const controller = scope.AbortController === undefined ? null : new scope.AbortController();
|
|
94
|
+
abort = controller;
|
|
95
|
+
const timeout = controller === null ? null : scope.setTimeout(() => controller.abort(), STATUS_TIMEOUT_MS);
|
|
96
|
+
try {
|
|
97
|
+
const response = await scope.fetch(config.statusUrl, {
|
|
98
|
+
headers: { Accept: 'application/json', Authorization: `Bearer ${config.token}` },
|
|
99
|
+
cache: 'no-store',
|
|
100
|
+
...(controller === null ? {} : { signal: controller.signal }),
|
|
101
|
+
});
|
|
102
|
+
await answer(response);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Offline for a moment, or the request hung and was aborted: try again on the next beat.
|
|
106
|
+
schedule();
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
inFlight = false;
|
|
110
|
+
abort = null;
|
|
111
|
+
if (timeout !== null)
|
|
112
|
+
scope.clearTimeout(timeout);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const answer = async (response) => {
|
|
116
|
+
if (ended)
|
|
117
|
+
return;
|
|
118
|
+
if (response.ok)
|
|
119
|
+
backOffUntil = 0;
|
|
120
|
+
if (response.status === 429) {
|
|
121
|
+
const seconds = (response.headers.get('Retry-After') ?? '').trim();
|
|
122
|
+
if (/^\d+$/.test(seconds))
|
|
123
|
+
backOffUntil = now() + Math.min(RETRY_AFTER_CAP_MS, Number(seconds) * 1000);
|
|
124
|
+
}
|
|
125
|
+
if (response.ok || response.status === 429 || response.status >= 500) {
|
|
126
|
+
schedule();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let problem = {};
|
|
130
|
+
try {
|
|
131
|
+
const body = await response.json();
|
|
132
|
+
if (body !== null && typeof body === 'object')
|
|
133
|
+
problem = body;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// No problem body: treated as closed below.
|
|
137
|
+
}
|
|
138
|
+
const code = problem.code;
|
|
139
|
+
let newerUrl = null;
|
|
140
|
+
if (code === 'trial_expired') {
|
|
141
|
+
end('expired');
|
|
142
|
+
}
|
|
143
|
+
else if (code === 'trial_elsewhere') {
|
|
144
|
+
end('elsewhere', [NL.trial.reload, () => scope.location.reload()]);
|
|
145
|
+
}
|
|
146
|
+
else if (code === 'trial_replaced' && (newerUrl = trustedNewer(problem.newer, config.dmpUrl)) !== null) {
|
|
147
|
+
const newer = newerUrl.href;
|
|
148
|
+
if (problem.wider === false) {
|
|
149
|
+
end('refreshing');
|
|
150
|
+
scope.location.assign(newer);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Reads more than the open one: only the maker's click may widen what this tab reads.
|
|
154
|
+
end('wider', [NL.trial.load, () => scope.location.assign(newer)]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
end('closed');
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
function onVisibility() {
|
|
162
|
+
if (ended || inFlight)
|
|
163
|
+
return;
|
|
164
|
+
if (!visible()) {
|
|
165
|
+
schedule();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Back in view during a back-off: wait out its rest.
|
|
169
|
+
if (backOffUntil > now()) {
|
|
170
|
+
schedule(0);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Back in view: ask right away instead of waiting out the hidden interval.
|
|
174
|
+
if (timer !== null)
|
|
175
|
+
scope.clearTimeout(timer);
|
|
176
|
+
void tick();
|
|
177
|
+
}
|
|
178
|
+
doc.addEventListener('visibilitychange', onVisibility);
|
|
179
|
+
if (doc.body === null) {
|
|
180
|
+
doc.addEventListener('DOMContentLoaded', insert, { once: true });
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
insert();
|
|
184
|
+
}
|
|
185
|
+
schedule();
|
|
186
|
+
return stop;
|
|
187
|
+
}
|
|
188
|
+
/** Only the DMP's own open route: same origin as `dmpUrl`, under `/apps/proef/`. */
|
|
189
|
+
function trustedNewer(newer, dmpUrl) {
|
|
190
|
+
if (typeof newer !== 'string')
|
|
191
|
+
return null;
|
|
192
|
+
try {
|
|
193
|
+
const url = new URL(newer);
|
|
194
|
+
return url.origin === new URL(dmpUrl).origin && url.pathname.startsWith('/apps/proef/') ? url : null;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
package/dist/ui/write.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ export declare function attemptFor(earlier: Attempt | null, fingerprint: string)
|
|
|
12
12
|
export declare function showAlert(doc: Document, target: HTMLElement, message: string, action?: HTMLElement | null): void;
|
|
13
13
|
/**
|
|
14
14
|
* Shows why a write failed: a 422 on the fields it names, with the form-level line, and the messages for fields
|
|
15
|
-
* without an editor listed in the alert (`label` names them); anything else as one line.
|
|
16
|
-
*
|
|
15
|
+
* without an editor listed in the alert (`label` names them); anything else as one line. Only the kind of stateFor
|
|
16
|
+
* is used: the alert never shows an error's own text.
|
|
17
17
|
*/
|
|
18
18
|
export declare function showWriteFailure(doc: Document, alert: HTMLElement, error: unknown, editors: Editor[], label: (field: string) => string): void;
|
|
19
19
|
/** Who is signed in, as a key: null when nobody is. A write must not go out as somebody who signed in since. */
|
package/dist/ui/write.js
CHANGED
|
@@ -29,8 +29,8 @@ export function showAlert(doc, target, message, action) {
|
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* Shows why a write failed: a 422 on the fields it names, with the form-level line, and the messages for fields
|
|
32
|
-
* without an editor listed in the alert (`label` names them); anything else as one line.
|
|
33
|
-
*
|
|
32
|
+
* without an editor listed in the alert (`label` names them); anything else as one line. Only the kind of stateFor
|
|
33
|
+
* is used: the alert never shows an error's own text.
|
|
34
34
|
*/
|
|
35
35
|
export function showWriteFailure(doc, alert, error, editors, label) {
|
|
36
36
|
const errors = fieldErrors(error);
|
package/dist/version.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** This package's version. A test holds it equal to package.json's. */
|
|
2
|
-
export declare const VERSION = "1.1.0-rc.
|
|
2
|
+
export declare const VERSION = "1.1.0-rc.3";
|
|
3
3
|
/** The `"sdk"` value a `cube-app.json` manifest names for the API `CubeApp` offers here. */
|
|
4
4
|
export declare const MANIFEST_SDK = "1";
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** This package's version. A test holds it equal to package.json's. */
|
|
2
|
-
export const VERSION = '1.1.0-rc.
|
|
2
|
+
export const VERSION = '1.1.0-rc.3';
|
|
3
3
|
/** The `"sdk"` value a `cube-app.json` manifest names for the API `CubeApp` offers here. */
|
|
4
4
|
export const MANIFEST_SDK = '1';
|
package/dist/wireframe.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DmpError } from './errors.js';
|
|
2
|
-
import { PreviewStore } from './preview.js';
|
|
2
|
+
import { compare, PreviewStore } from './preview.js';
|
|
3
3
|
import { isoDate } from './ui/format.js';
|
|
4
4
|
const FORMATS = {
|
|
5
5
|
tekst: 'text',
|
|
@@ -11,6 +11,8 @@ const FORMATS = {
|
|
|
11
11
|
};
|
|
12
12
|
const filled = (value) => typeof value === 'string' && value !== '';
|
|
13
13
|
const own = (row, key) => Object.prototype.hasOwnProperty.call(row, key);
|
|
14
|
+
// `a, -b` and ['a', '-b'] alike: the items of a comma list, as the server reads it.
|
|
15
|
+
const items = (value = []) => [value].flat().join(',').split(',').map((item) => item.trim());
|
|
14
16
|
function isGap(value) {
|
|
15
17
|
if (typeof value !== 'object' || value === null) {
|
|
16
18
|
return false;
|
|
@@ -29,11 +31,12 @@ function isGap(value) {
|
|
|
29
31
|
}
|
|
30
32
|
/** The valid gap entries of `ontbreekt`; malformed ones are dropped with a single warning. */
|
|
31
33
|
export function readGaps(value) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const gaps = [];
|
|
35
|
+
const dropped = [];
|
|
36
|
+
// Anything but a list is one malformed entry: a typo must not silently mean "nothing is missing".
|
|
37
|
+
for (const entry of Array.isArray(value) ? value : value == null ? [] : [value]) {
|
|
38
|
+
(isGap(entry) ? gaps : dropped).push(entry);
|
|
34
39
|
}
|
|
35
|
-
const gaps = value.filter(isGap);
|
|
36
|
-
const dropped = value.filter((entry) => !isGap(entry));
|
|
37
40
|
if (dropped.length > 0) {
|
|
38
41
|
console.warn(`cube-app.json ontbreekt: ${dropped.length} malformed entries ignored`, dropped);
|
|
39
42
|
}
|
|
@@ -73,10 +76,11 @@ export function withGaps(schema, gaps) {
|
|
|
73
76
|
const names = added.map((field) => field.name);
|
|
74
77
|
return {
|
|
75
78
|
...schema,
|
|
76
|
-
//
|
|
79
|
+
// Money is not recognisable by its values, dates are (inferFormat): only an amount gets its format here, and a
|
|
80
|
+
// stale `datum` gap must not drop a live date-time's time.
|
|
77
81
|
fields: [...schema.fields.map((field) => {
|
|
78
82
|
const gap = gaps.find(({ resource, veld }) => resource === schema.name && veld === field.name);
|
|
79
|
-
return gap
|
|
83
|
+
return gap?.type === 'bedrag' && field.format == null ? { ...field, format: 'currency' } : field;
|
|
80
84
|
}), ...added],
|
|
81
85
|
// An empty summary means "every field of a row", which already holds the invented ones.
|
|
82
86
|
summary: schema.summary.length > 0 ? [...schema.summary, ...names] : schema.summary,
|
|
@@ -141,13 +145,14 @@ async function locally(name, rows, { filter = {}, sort = [] }) {
|
|
|
141
145
|
export function mixedResource(live, gaps, mock, mockRows) {
|
|
142
146
|
const { name } = live;
|
|
143
147
|
const missing = new Map(gaps.filter((gap) => gap.resource === name && gap.veld !== undefined).map((gap) => [gap.veld, gap]));
|
|
144
|
-
// `-field`, `sum(field)` and `field.month`
|
|
145
|
-
|
|
148
|
+
// `-field`, `sum(field)` and `field.month` name `field`; `count` and `created_at.month` do not name a field
|
|
149
|
+
// called `count` or `month`.
|
|
150
|
+
const names = (item) => missing.has(/^[-+]?(?:\w+\()?([^.()]*)/.exec(item)?.[1] ?? '');
|
|
146
151
|
const split = (query = {}) => {
|
|
147
152
|
const selected = selection(query, name);
|
|
148
153
|
const kept = selected?.filter((field) => !missing.has(field));
|
|
149
154
|
const filters = Object.entries(query.filter ?? {});
|
|
150
|
-
const sort =
|
|
155
|
+
const sort = items(query.sort).filter((item) => item !== '');
|
|
151
156
|
// The live part names no missing field; the local part only missing ones.
|
|
152
157
|
const part = (local) => ({
|
|
153
158
|
filter: Object.fromEntries(filters.filter(([key]) => missing.has(key) === local)),
|
|
@@ -181,7 +186,15 @@ export function mixedResource(live, gaps, mock, mockRows) {
|
|
|
181
186
|
}
|
|
182
187
|
}
|
|
183
188
|
}
|
|
184
|
-
|
|
189
|
+
const { meta } = page;
|
|
190
|
+
return {
|
|
191
|
+
...page,
|
|
192
|
+
data: data,
|
|
193
|
+
// A filter on a missing field covers this page only, so the count and the last page say so too.
|
|
194
|
+
meta: Object.keys(local.filter).length > 0
|
|
195
|
+
? { ...meta, page: { ...meta.page, total: data.length, pages: meta.page.number } }
|
|
196
|
+
: meta,
|
|
197
|
+
};
|
|
185
198
|
};
|
|
186
199
|
const list = async (query, init) => {
|
|
187
200
|
const parts = split(query);
|
|
@@ -206,9 +219,15 @@ export function mixedResource(live, gaps, mock, mockRows) {
|
|
|
206
219
|
const parts = split(query);
|
|
207
220
|
const response = await live.get(id, parts.live, init);
|
|
208
221
|
fill(response.data, parts.shown);
|
|
222
|
+
// The id was only asked for the invented values.
|
|
223
|
+
if (!parts.shown('id')) {
|
|
224
|
+
delete response.data.id;
|
|
225
|
+
}
|
|
209
226
|
return response;
|
|
210
227
|
},
|
|
211
|
-
aggregate: (query, init) => [query.fn, query.group
|
|
228
|
+
aggregate: (query, init) => [items(query.fn).map((fn) => fn.replace(/^\w+$/, '')), items(query.group), Object.keys(query.filter ?? {})]
|
|
229
|
+
.flat()
|
|
230
|
+
.some(names)
|
|
212
231
|
? mock.resource(name).aggregate(query)
|
|
213
232
|
: live.aggregate(query, init),
|
|
214
233
|
};
|
|
@@ -253,9 +272,27 @@ export function simulatedWrites(client, name) {
|
|
|
253
272
|
.map((row) => local.get(key(row)) ?? row);
|
|
254
273
|
const joined = fitting.filter((row) => !served.has(key(row)) && !listed.has(key(row)));
|
|
255
274
|
const total = page.total - [...served].filter((id) => !fits.has(id)).length + joined.length;
|
|
275
|
+
let data = page.number === 1 ? [...joined, ...kept] : kept;
|
|
276
|
+
// No sort reads as one empty field, which no row has: then nothing moves.
|
|
277
|
+
const sorts = items(query.sort)
|
|
278
|
+
.map((item) => [item.replace(/^[-+]/, ''), item.startsWith('-') ? -1 : 1]);
|
|
279
|
+
// A row written here that the server did not list (created, or joining the list) or that changed in a sort
|
|
280
|
+
// field is placed by the sort; the others keep the server's order, whose collation may differ from ours.
|
|
281
|
+
const placed = (row) => local.has(key(row))
|
|
282
|
+
&& (!listed.has(key(row)) || sorts.some(([field]) => origins.get(key(row))?.[field] !== row[field]));
|
|
283
|
+
const server = data.filter((row) => !placed(row));
|
|
284
|
+
// Only when the server's rows carry every sort field to compare on; a placed row without one sorts as empty.
|
|
285
|
+
if (sorts.every(([field]) => server.every((row) => own(row, field)))) {
|
|
286
|
+
for (const row of data.filter(placed)) {
|
|
287
|
+
// Before the first row (server or placed) it sorts before; -1 >>> 0 is past the end, so it appends.
|
|
288
|
+
const at = server.findIndex((other) => sorts.reduce((result, [field, sign]) => result || sign * compare(row[field], other[field]), 0) < 0);
|
|
289
|
+
server.splice(at >>> 0, 0, row);
|
|
290
|
+
}
|
|
291
|
+
data = server;
|
|
292
|
+
}
|
|
256
293
|
return {
|
|
257
294
|
...response,
|
|
258
|
-
data:
|
|
295
|
+
data: data.map((row) => pick(row, query)),
|
|
259
296
|
meta: { ...response.meta, page: { ...page, total } },
|
|
260
297
|
};
|
|
261
298
|
};
|
|
@@ -268,7 +305,7 @@ export function simulatedWrites(client, name) {
|
|
|
268
305
|
return copy === undefined ? client.get(id, query, init) : answer(200, pick(copy, query));
|
|
269
306
|
};
|
|
270
307
|
// The row as it is here; a real one's server copy is kept the first time it changes.
|
|
271
|
-
const
|
|
308
|
+
const current = async (id) => {
|
|
272
309
|
const { data } = await get(id);
|
|
273
310
|
if (!local.has(String(id))) {
|
|
274
311
|
origins.set(String(id), data);
|
|
@@ -299,14 +336,14 @@ export function simulatedWrites(client, name) {
|
|
|
299
336
|
return answer(201, { ...row });
|
|
300
337
|
},
|
|
301
338
|
async update(id, body) {
|
|
302
|
-
const data = await
|
|
339
|
+
const data = await current(id);
|
|
303
340
|
const row = { ...data, ...body, id: data.id };
|
|
304
341
|
local.set(String(id), row);
|
|
305
342
|
return answer(200, { ...row });
|
|
306
343
|
},
|
|
307
344
|
// As the server: an id that is not there is a 404, and a delete never lowers a count for nothing.
|
|
308
345
|
async delete(id) {
|
|
309
|
-
await
|
|
346
|
+
await current(id);
|
|
310
347
|
local.delete(String(id));
|
|
311
348
|
deleted.add(String(id));
|
|
312
349
|
return answer(204, null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cube-nl/dmp-sdk",
|
|
3
|
-
"version": "1.1.0-rc.
|
|
3
|
+
"version": "1.1.0-rc.3",
|
|
4
4
|
"description": "Dependency-free TypeScript client for the Cube DMP REST API (cube/api), with OAuth 2.1 + PKCE, and CubeApp for no-build apps (preview on mock data, live on the DMP).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
"access": "public",
|
|
26
26
|
"tag": "next"
|
|
27
27
|
},
|
|
28
|
-
"sideEffects":
|
|
28
|
+
"sideEffects": [
|
|
29
|
+
"./dist/trial.js",
|
|
30
|
+
"./dist/cube-sdk.js"
|
|
31
|
+
],
|
|
29
32
|
"engines": {
|
|
30
33
|
"node": ">=20"
|
|
31
34
|
},
|