@cube-nl/dmp-sdk 1.1.0-rc.1 → 1.1.0-rc.2

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/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
- if (!Array.isArray(value)) {
33
- return [];
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
- // Mock data may hold a missing field without its format: an amount there still shows as money.
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 !== undefined && field.format == null ? { ...field, format: shown(gap.type) } : field;
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` all name `field`.
145
- const names = (item) => item.split(/[-+(),.]/).some((part) => missing.has(part));
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 = [query.sort ?? []].flat();
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
- return { ...page, data: data };
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 ?? [], Object.keys(query.filter ?? {})].flat().some(names)
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: (page.number === 1 ? [...joined, ...kept] : kept).map((row) => pick(row, query)),
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 own = async (id) => {
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 own(id);
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 own(id);
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.1",
3
+ "version": "1.1.0-rc.2",
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": false,
28
+ "sideEffects": [
29
+ "./dist/trial.js",
30
+ "./dist/cube-sdk.js"
31
+ ],
29
32
  "engines": {
30
33
  "node": ">=20"
31
34
  },