@cube-nl/dmp-sdk 1.0.0-rc.3 → 1.0.0-rc.4
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 +55 -25
- package/dist/app.d.ts +2 -2
- package/dist/app.js +1 -1
- package/dist/cube-sdk.js +2 -2
- package/dist/preview.d.ts +9 -0
- package/dist/preview.js +160 -33
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @cube/dmp-sdk
|
|
1
|
+
# @cube-nl/dmp-sdk
|
|
2
2
|
|
|
3
3
|
**Nederlands.** De TypeScript-SDK voor het DMP: één client waarmee een app de REST-API van
|
|
4
4
|
`cube/api` aanroept, in de browser of in Node 20+. Zonder runtime-dependencies: alleen `fetch`,
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
een persoonlijk token voor een PoC. Het token blijft standaard alleen in het geheugen.
|
|
10
10
|
|
|
11
11
|
Dit pakket vervangt `dmp.js` en `oauth.js` uit de hackathon-starter van `cube/resources`
|
|
12
|
-
(`docs/hackathon/starter/public/`): dezelfde logica, getypt en getest. Het staat
|
|
13
|
-
|
|
12
|
+
(`docs/hackathon/starter/public/`): dezelfde logica, getypt en getest. Het staat openbaar op npm:
|
|
13
|
+
`npm install @cube-nl/dmp-sdk@next` (zolang het een release candidate is), of als losse
|
|
14
|
+
`cube-sdk.js` via jsDelivr of unpkg.
|
|
14
15
|
|
|
15
16
|
Besluit OP-05: **één client, resources als eigenschap** —
|
|
16
17
|
`createClient(...).resources('projects').list()`, geen ingang per soort.
|
|
@@ -34,16 +35,28 @@ The rest of this document is in English.
|
|
|
34
35
|
|
|
35
36
|
## Install
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
Published on the public npm registry as [`@cube-nl/dmp-sdk`](https://www.npmjs.com/package/@cube-nl/dmp-sdk):
|
|
38
39
|
|
|
39
40
|
```bash
|
|
40
|
-
npm install
|
|
41
|
-
|
|
42
|
-
npm install ./cube-dmp-sdk-1.0.0-rc.3.tgz
|
|
41
|
+
npm install @cube-nl/dmp-sdk@next # while 1.0.0 is a release candidate
|
|
42
|
+
npm install @cube-nl/dmp-sdk # once a stable version is out
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
its own `.d.ts`; there is no CommonJS build.
|
|
45
|
+
Release candidates go to the `next` dist-tag, stable versions to `latest`. The package is an ES
|
|
46
|
+
module with its own `.d.ts`; there is no CommonJS build.
|
|
47
|
+
|
|
48
|
+
For an app without a build step, the single-file browser build is on the npm CDNs. Pin an exact
|
|
49
|
+
version:
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<script src="https://cdn.jsdelivr.net/npm/@cube-nl/dmp-sdk@1.0.0-rc.4/dist/cube-sdk.js"></script>
|
|
53
|
+
<!-- or -->
|
|
54
|
+
<script src="https://unpkg.com/@cube-nl/dmp-sdk@1.0.0-rc.4/dist/cube-sdk.js"></script>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Fallback, for an unreleased commit: `npm install
|
|
58
|
+
git+ssh://git@gitlab.cube.nl/cube/packages/laravel/filament/dmp/sdk-js.git#5.0.x` (needs access to
|
|
59
|
+
our GitLab; `prepare` builds `dist/`).
|
|
47
60
|
|
|
48
61
|
## Quick start
|
|
49
62
|
|
|
@@ -51,7 +64,7 @@ These ten lines ran as they are (24-09-2026, Node 24, installed from `npm pack`)
|
|
|
51
64
|
hackathon demo DMP on `127.0.0.1:8000`, with a personal token in `DMP_TOKEN`:
|
|
52
65
|
|
|
53
66
|
```js
|
|
54
|
-
import { createClient } from '@cube/dmp-sdk';
|
|
67
|
+
import { createClient } from '@cube-nl/dmp-sdk';
|
|
55
68
|
|
|
56
69
|
const dmp = createClient({ baseUrl: 'http://127.0.0.1:8000/apps/api/v1', auth: process.env.DMP_TOKEN });
|
|
57
70
|
|
|
@@ -97,7 +110,7 @@ Every request carries `Authorization: Bearer <token>`. The API refuses a browser
|
|
|
97
110
|
### OAuth 2.1 + PKCE (the right way for an app)
|
|
98
111
|
|
|
99
112
|
```ts
|
|
100
|
-
import { createClient, createOAuth } from '@cube/dmp-sdk';
|
|
113
|
+
import { createClient, createOAuth } from '@cube-nl/dmp-sdk';
|
|
101
114
|
|
|
102
115
|
const oauth = createOAuth({
|
|
103
116
|
issuer: 'https://dmp.example.nl', // discovery: {issuer}/.well-known/oauth-authorization-server
|
|
@@ -181,7 +194,7 @@ Token limits the API applies to an OAuth token: it reaches **no custom operation
|
|
|
181
194
|
### A personal token or an API-client token
|
|
182
195
|
|
|
183
196
|
```ts
|
|
184
|
-
import { bearer, createClient } from '@cube/dmp-sdk';
|
|
197
|
+
import { bearer, createClient } from '@cube-nl/dmp-sdk';
|
|
185
198
|
|
|
186
199
|
createClient({ baseUrl, auth: process.env.DMP_TOKEN }); // a string
|
|
187
200
|
createClient({ baseUrl, auth: () => tokenFromSomewhere() }); // read on every call
|
|
@@ -282,7 +295,7 @@ not from assumptions.
|
|
|
282
295
|
## Writing
|
|
283
296
|
|
|
284
297
|
```ts
|
|
285
|
-
import { newIdempotencyKey } from '@cube/dmp-sdk';
|
|
298
|
+
import { newIdempotencyKey } from '@cube-nl/dmp-sdk';
|
|
286
299
|
|
|
287
300
|
const projects = dmp.resources('projects');
|
|
288
301
|
|
|
@@ -344,7 +357,7 @@ name, and an OAuth token reaches none. Names starting with `_` are reserved and
|
|
|
344
357
|
Every non-2xx answer throws a `DmpError`:
|
|
345
358
|
|
|
346
359
|
```ts
|
|
347
|
-
import { isDmpError } from '@cube/dmp-sdk';
|
|
360
|
+
import { isDmpError } from '@cube-nl/dmp-sdk';
|
|
348
361
|
|
|
349
362
|
try {
|
|
350
363
|
await dmp.resources('projects').list({ sort: 'nope' });
|
|
@@ -548,7 +561,14 @@ the REST face reads it:
|
|
|
548
561
|
Laravel's sentence per field; an empty body `422 validation_failed`. `ifMatch` with a stale
|
|
549
562
|
`recordVersion` is `409 conflict`; `idempotencyKey` replays the first answer.
|
|
550
563
|
- `aggregate` with `count`, `count(f)`, `sum(f)`, `avg(f)`, `min(f)`, `max(f)`, grouped on fields
|
|
551
|
-
and on `.
|
|
564
|
+
and on `.day`, `.week` (ISO, `2026-W23`), `.month`, `.quarter` (`2026-Q2`) and `.year`; a date-time is
|
|
565
|
+
bucketed by the date as written. A call needs `aggregate` in the manifest's `uses` (`list` alone no longer
|
|
566
|
+
suffices). Grouping is limited to the schema's `groupable` fields and a function to what `aggregatable` allows
|
|
567
|
+
(absent: every field, every function; a bare `count` is always allowed). Groups come ascending by their values,
|
|
568
|
+
nulls first; a `sum` of nothing is `null`; a whole-set aggregate over no rows answers one row. At most 5
|
|
569
|
+
dimensions and 1000 groups; a repeated entry is refused.
|
|
570
|
+
- A field with `choices` in the schema: a value outside `values` (else the field's own `enum`) is
|
|
571
|
+
`422 validation_failed` ("The selected status is invalid."), and a choice that is not `writable` is read-only.
|
|
552
572
|
- With a `cube-app.json` next to the page (or the `manifest` option), a call outside its `uses` is
|
|
553
573
|
`403 client_scope_exceeded`, as the live client is capped to it.
|
|
554
574
|
- Signed out (`signOut()`), every call is `401 token_invalid`.
|
|
@@ -586,13 +606,17 @@ synthetic rows.
|
|
|
586
606
|
Ship it as `cube-mock.json` next to `index.html`, or register it in a script before the app's
|
|
587
607
|
module runs: `CubeApp.mock({ ... })`. A resource without a `schema` takes its fields from its rows.
|
|
588
608
|
|
|
609
|
+
The schema may also carry three keys that `cube.mock_data` writes: `groupable` (fields an aggregate may group on),
|
|
610
|
+
`aggregatable` (per field, the functions allowed) and `choices` (per field, `{ "values": [...], "writable": true }`).
|
|
611
|
+
Without them the preview is permissive: every field, every function; only a field's own `enum` is checked on writes.
|
|
612
|
+
|
|
589
613
|
### `CubeApp` API
|
|
590
614
|
|
|
591
615
|
| | |
|
|
592
616
|
|---|---|
|
|
593
617
|
| `CubeApp.connect(options?)` | `Promise<CubeAppSession>`. Options: `mock`, `manifest` (an object, or `false` to check nothing), `pageUrl` (default the page's URL), `fetch`, `tokenStorage` (`'session'`, the default, or `'memory'`), `autoSignIn` (default `false`) |
|
|
594
618
|
| `CubeApp.mock(data)` | Registers preview data for the next `connect()`; `null` forgets it |
|
|
595
|
-
| `CubeApp.version` | This SDK's version, `'0.
|
|
619
|
+
| `CubeApp.version` | This SDK's version, e.g. `'1.0.0-rc.4'` |
|
|
596
620
|
| `CubeApp.sdk` | The manifest `"sdk"` this build serves, `'1'` |
|
|
597
621
|
| `session.mode` | `'preview'` or `'live'` |
|
|
598
622
|
| `session.user()` | `{ id, name, scopes }` or `null` |
|
|
@@ -607,21 +631,20 @@ memory default: a no-build app reloads often, and a redirect round trip on every
|
|
|
607
631
|
its users than a token that lives as long as the tab. `tokenStorage: 'memory'` opts out. `user()` reads
|
|
608
632
|
the id and scopes from the access token for display; the DMP checks the token on every request.
|
|
609
633
|
|
|
610
|
-
`CubeApp` is also exported from the ES module (`import { CubeApp } from '@cube/dmp-sdk'`) for an
|
|
634
|
+
`CubeApp` is also exported from the ES module (`import { CubeApp } from '@cube-nl/dmp-sdk'`) for an
|
|
611
635
|
app with a build (route A).
|
|
612
636
|
|
|
613
637
|
### For the host: serving `cube-sdk.js`
|
|
614
638
|
|
|
615
639
|
| | |
|
|
616
640
|
|---|---|
|
|
617
|
-
| File | `dist/cube-sdk.js` in the package, `@cube/dmp-sdk/cube-sdk.js` as an export; the CI's `pack` job also keeps it as an artifact |
|
|
618
|
-
| Size |
|
|
619
|
-
| Version | The banner's first line, `/*! @cube/dmp-sdk 0.
|
|
641
|
+
| File | `dist/cube-sdk.js` in the package, `@cube-nl/dmp-sdk/cube-sdk.js` as an export; on jsDelivr/unpkg as `@cube-nl/dmp-sdk@<version>/dist/cube-sdk.js`; the CI's `pack` job also keeps it as an artifact |
|
|
642
|
+
| Size | 31.4 KiB minified, 11.3 KiB gzipped (1.0.0-rc.4) |
|
|
643
|
+
| Version | The banner's first line, `/*! @cube-nl/dmp-sdk 1.0.0-rc.4 cube-sdk.js … */`, and `CubeApp.version` |
|
|
620
644
|
|
|
621
|
-
Pin it by copying the built file
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
time.
|
|
645
|
+
Pin it by copying the built file of a known version into the host, rather than fetching it at
|
|
646
|
+
runtime: `npm pack @cube-nl/dmp-sdk@<version>` downloads the tarball, `package/dist/cube-sdk.js` is
|
|
647
|
+
the file. Nothing from a CDN, nothing at request time.
|
|
625
648
|
|
|
626
649
|
## What this SDK deliberately does not do
|
|
627
650
|
|
|
@@ -636,7 +659,6 @@ time.
|
|
|
636
659
|
- **No token revocation call**: `cube/oauth-server` publishes no revocation endpoint. `signOut()`
|
|
637
660
|
forgets locally; the person revokes the connection in the DMP under *Verbonden apps*.
|
|
638
661
|
- **No `_history`**: the API has not built it. `request()` reaches any path once it exists.
|
|
639
|
-
- **Not published** to any registry yet.
|
|
640
662
|
|
|
641
663
|
## Development
|
|
642
664
|
|
|
@@ -654,6 +676,14 @@ then says what the SDK still reads wrong.
|
|
|
654
676
|
TypeScript and esbuild (which bundles `dist/cube-sdk.js`, see `scripts/build-browser.mjs`) are the only
|
|
655
677
|
dev dependencies; there are no runtime dependencies, and there should not be.
|
|
656
678
|
|
|
679
|
+
### Releasing
|
|
680
|
+
|
|
681
|
+
Bump `version` in `package.json` and `src/version.ts` (a test holds them equal), add the CHANGELOG
|
|
682
|
+
entry, merge, then push a tag that equals the version (e.g. `1.0.0-rc.4`). The tag pipeline's
|
|
683
|
+
`publish` job checks that, builds, and runs `npm publish`: a version with a `-` goes to the `next`
|
|
684
|
+
dist-tag, any other to `latest`. It needs the masked, protected CI variable `NPM_TOKEN` (an npm
|
|
685
|
+
granular access token with write access to the `cube-nl` org).
|
|
686
|
+
|
|
657
687
|
## Integration tests
|
|
658
688
|
|
|
659
689
|
`test/integration.test.js` runs the SDK against a real DMP. `npm test` includes it, and it skips
|
package/dist/app.d.ts
CHANGED
|
@@ -78,8 +78,8 @@ declare function mock(data: MockData | null): void;
|
|
|
78
78
|
export declare const CubeApp: Readonly<{
|
|
79
79
|
connect: typeof connect;
|
|
80
80
|
mock: typeof mock;
|
|
81
|
-
/** This SDK's version, e.g. `'1.0.0-rc.
|
|
82
|
-
version: "1.0.0-rc.
|
|
81
|
+
/** This SDK's version, e.g. `'1.0.0-rc.4'`. */
|
|
82
|
+
version: "1.0.0-rc.4";
|
|
83
83
|
/** The manifest `"sdk"` value this build serves. */
|
|
84
84
|
sdk: "1";
|
|
85
85
|
}>;
|
package/dist/app.js
CHANGED
|
@@ -238,7 +238,7 @@ function mock(data) {
|
|
|
238
238
|
export const CubeApp = Object.freeze({
|
|
239
239
|
connect,
|
|
240
240
|
mock,
|
|
241
|
-
/** This SDK's version, e.g. `'1.0.0-rc.
|
|
241
|
+
/** This SDK's version, e.g. `'1.0.0-rc.4'`. */
|
|
242
242
|
version: VERSION,
|
|
243
243
|
/** The manifest `"sdk"` value this build serves. */
|
|
244
244
|
sdk: MANIFEST_SDK,
|
package/dist/cube-sdk.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/*! @cube/dmp-sdk 1.0.0-rc.
|
|
2
|
-
"use strict";(()=>{var be=Object.defineProperty;var ke=(e,n,t)=>n in e?be(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t;var k=(e,n,t)=>ke(e,typeof n!="symbol"?n+"":n,t);var R=class extends Error{constructor(t,r,s){let i=s!==null&&typeof s=="object"?s:{},o=A(i.detail)??A(i.error_description);super(o??`HTTP ${t}`);k(this,"status");k(this,"code");k(this,"reason");k(this,"type");k(this,"title");k(this,"detail");k(this,"errors");k(this,"retryAfter");k(this,"recordVersion");k(this,"allow");k(this,"headers");k(this,"body");this.name="DmpError",this.status=t,this.code=A(i.code)??A(i.error),this.reason=A(i.reason),this.type=A(i.type),this.title=A(i.title),this.detail=o,this.errors=Re(i.errors)?i.errors:null,this.retryAfter=Te(r.get("Retry-After")),this.recordVersion=r.get("X-Dmp-Record-Version"),this.allow=r.has("Allow")?(r.get("Allow")??"").split(",").map(a=>a.trim()).filter(a=>a!==""):null,this.headers=r,this.body=s}get isProblem(){return this.code!==null}};function A(e){return typeof e=="string"?e:null}function Re(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function Te(e){if(e===null||e.trim()==="")return null;let n=Number(e);if(Number.isFinite(n))return n;let t=Date.parse(e);return Number.isNaN(t)?null:Math.max(0,Math.ceil((t-Date.now())/1e3))}function Y(e){return{async getAccessToken(){let n=typeof e=="function"?await e():e;return n===void 0||n===""?null:n}}}var $="cube-dmp-sdk.pending-login";function Z(e){let n=e.fetch??globalThis.fetch.bind(globalThis),t=e.tokenStore??M(),r=e.pendingStore??ve(),s=(e.refreshLeewaySeconds??30)*1e3,i=typeof e.scopes=="string"?e.scopes:e.scopes.join(" "),o=e.metadata??{},a=Se(o)?Promise.resolve({...o,issuer:o.issuer??e.issuer}):null,l=null,c=()=>(a??(a=_e(e.issuer,n).then(p=>({...p,...o}),p=>{throw a=null,p})),a),g=async(p,d)=>{let{token_endpoint:u}=await c(),f=new URLSearchParams(p);e.clientSecret!==void 0&&f.set("client_secret",e.clientSecret);let m=await n(u,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/x-www-form-urlencoded"},body:f.toString()}),y=await ee(m);if(!m.ok||y===null||typeof y!="object"||typeof y.access_token!="string")throw new R(m.status,m.headers,y);let T={accessToken:y.access_token,refreshToken:typeof y.refresh_token=="string"?y.refresh_token:null,expiresAt:typeof y.expires_in=="number"?Date.now()+y.expires_in*1e3:null,scope:typeof y.scope=="string"?y.scope:d};return await t.set(T),T},b=()=>(l??(l=(async()=>{let p=await t.get();if(p?.refreshToken==null)throw new TypeError("No refresh token: sign in again.");try{return await g({grant_type:"refresh_token",refresh_token:p.refreshToken,client_id:e.clientId},p.scope)}catch(d){throw d instanceof R&&d.status>=400&&d.status<500&&await t.set(null),d}})().finally(()=>{l=null})),l),w=async(p={})=>{let d=await c();if(!(d.code_challenge_methods_supported??[]).includes("S256"))throw new TypeError("The authorization server does not offer PKCE S256; this SDK will not sign in without it.");let u=B(64),f=p.state??B(32),m=ne(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(u))),y={verifier:u,state:f,redirectUri:e.redirectUri,clientId:e.clientId};r.setItem($,JSON.stringify(y));let T=new URL(d.authorization_endpoint);return T.search=new URLSearchParams({...p.extraParams,response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUri,scope:i,state:f,code_challenge:m,code_challenge_method:"S256"}).toString(),T.toString()};return{discover:c,authorizationUrl:w,async signIn(p={}){let d=X();if(d===null)throw new TypeError("signIn() needs a browser; use authorizationUrl() and redirect yourself.");d.assign(await w(p))},async handleCallback(p){let d=p??X()?.href;if(d===void 0)throw new TypeError("handleCallback() needs the callback URL outside a browser.");let u=new URL(d),f=new URLSearchParams(u.search);if(!Oe(u,e.redirectUri)||!f.has("code")&&!f.has("error"))return null;if(p===void 0){for(let y of["code","state","error","error_description","iss"])u.searchParams.delete(y);globalThis.history?.replaceState(null,"",u.pathname+u.search+u.hash)}let m=De(r.getItem($));if(m===null||f.get("state")!==m.state)throw new TypeError("The state does not match: this callback does not belong to a sign-in started here.");if(r.removeItem($),f.has("error"))throw new R(400,new Headers,{error:f.get("error")??"access_denied",error_description:f.get("error_description")??void 0});return g({grant_type:"authorization_code",code:f.get("code")??"",redirect_uri:m.redirectUri,client_id:m.clientId,code_verifier:m.verifier},i)},refresh:b,async getTokens(){return t.get()},async setTokens(p){await t.set(p)},async signOut(){await t.set(null),r.removeItem($)},async getAccessToken(){let p=await t.get();if(p===null)return null;if(p.expiresAt!==null&&p.expiresAt-s<=Date.now()&&p.refreshToken!==null)try{return(await b()).accessToken}catch{return(await t.get())?.accessToken??null}return p.accessToken},async onUnauthorized(){if((await t.get())?.refreshToken==null)return!1;try{return await b(),!0}catch{return!1}}}}function Se(e){return typeof e.authorization_endpoint=="string"&&typeof e.token_endpoint=="string"}async function _e(e,n=globalThis.fetch.bind(globalThis)){let t=`${e.replace(/\/+$/,"")}/.well-known/oauth-authorization-server`,r=await n(t,{headers:{Accept:"application/json"}}),s=await ee(r);if(!r.ok||s===null||typeof s!="object")throw new R(r.status,r.headers,s);return s}function M(e=null){let n=e;return{get:()=>n,set:t=>{n=t}}}function Ae(){let e=new Map;return{getItem:n=>e.get(n)??null,setItem:(n,t)=>{e.set(n,t)},removeItem:n=>{e.delete(n)}}}function ve(){try{let e=globalThis.sessionStorage;if(e!==void 0)return e}catch{}return Ae()}function X(){let e=globalThis.location;return e!==void 0&&typeof e.assign=="function"?e:null}function Oe(e,n){let t=new URL(n),r=s=>s.replace(/\/+$/,"")||"/";return e.origin===t.origin&&r(e.pathname)===r(t.pathname)}function De(e){if(e===null)return null;try{let n=JSON.parse(e);return typeof n.verifier=="string"&&typeof n.state=="string"&&typeof n.redirectUri=="string"&&typeof n.clientId=="string"?n:null}catch{return null}}async function ee(e){let n=await e.text();if(n==="")return null;try{return JSON.parse(n)}catch{return n}}function B(e){return ne(crypto.getRandomValues(new Uint8Array(e)))}function ne(e){let n=e instanceof Uint8Array?e:new Uint8Array(e),t="";for(let r of n)t+=String.fromCharCode(r);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}var Pe=new Set(["in","between"]);function v(e,n={}){let t=new URLSearchParams;xe(t,e,n.fields),$e(t,n.filter),P(t,"sort",n.sort),P(t,"include",n.include),P(t,"with_count",n.withCount),Ue(t,n.has),P(t,"fn",n.fn),P(t,"group",n.group),n.page?.size!==void 0&&t.append("page[size]",String(n.page.size)),n.page?.number!==void 0&&t.append("page[number]",String(n.page.number));for(let[r,s]of Object.entries(n.params??{}))t.append(r,s);return t.toString().replace(/%5B/gi,"[").replace(/%5D/gi,"]").replace(/%2C/gi,",").replace(/%28/gi,"(").replace(/%29/gi,")")}function xe(e,n,t){if(t===void 0)return;let r=Ee(t)?{[n]:t}:t;for(let[s,i]of Object.entries(r))i.length>0&&e.append(`fields[${s}]`,i.join(","))}function $e(e,n){for(let[t,r]of Object.entries(n??{})){if(Array.isArray(r))throw new TypeError(`filter.${t}: a bare list names no operator and cube/api refuses it. Write { in: [...] } (or { between: [from, to] }).`);if(r==null)throw new TypeError(`filter.${t}: write { is: 'null' } or { is: 'not_null' } to filter on null.`);if(typeof r!="object"){e.append(`filter[${t}]`,F(r));continue}for(let[s,i]of Object.entries(r))i!==void 0&&e.append(`filter[${t}][${s}]`,Me(t,s,i))}}function Me(e,n,t){if(t===null){if(n!=="is")throw new TypeError(`filter.${e}.${n}: null is only meaningful under 'is'.`);return"null"}if(Array.isArray(t)){if(!Pe.has(n))throw new TypeError(`filter.${e}.${n}: takes one value; only 'in' and 'between' take a list.`);return t.map(F).join(",")}return F(t)}function Ue(e,n){for(let[t,r]of Object.entries(n??{}))r===!0?e.append(`has[${t}]`,"1"):typeof r=="number"?e.append(`has[${t}][min]`,String(r)):(r.min!==void 0&&e.append(`has[${t}][min]`,String(r.min)),r.max!==void 0&&e.append(`has[${t}][max]`,String(r.max)))}function P(e,n,t){if(t===void 0)return;let r=typeof t=="string"?t:t.filter(s=>s!=="").join(",");r!==""&&e.append(n,r)}function F(e){return typeof e=="boolean"?e?"1":"0":String(e)}function Ee(e){return Array.isArray(e)}var Ce=/^[a-z][a-z0-9_]*$/;function re(e){let n=e.baseUrl.replace(/\/+$/,""),t=e.fetch??globalThis.fetch.bind(globalThis),r=e.auth===void 0?null:typeof e.auth=="string"||typeof e.auth=="function"?Y(e.auth):e.auth,s=e.cache??(typeof document<"u"?"no-cache":void 0),i=async(a,l,{query:c,body:g,headers:b={},signal:w}={})=>{let p=`${n}${l.startsWith("/")?l:`/${l}`}${c?`?${c}`:""}`,d=async()=>{let y=r===null?null:await r.getAccessToken(),T={method:a,headers:{Accept:"application/json",...e.headers,...y!==null?{Authorization:`Bearer ${y}`}:{},...g!==void 0?{"Content-Type":"application/json"}:{},...b}};g!==void 0&&(T.body=JSON.stringify(g)),s!==void 0&&(T.cache=s),w!==void 0&&(T.signal=w);let we=Date.now(),H=await t(p,T);return e.onResponse?.({method:a,url:p,status:H.status,ms:Date.now()-we}),H},u=await d();u.status===401&&r?.onUnauthorized!==void 0&&await r.onUnauthorized()&&(await u.body?.cancel().catch(()=>{}),u=await d());let f=await Ie(u);if(!u.ok)throw new R(u.status,u.headers,f);let m=Le(f)?f:null;return{status:u.status,data:m!==null?m.data:f,meta:m?.meta??null,headers:u.headers,etag:u.headers.get("ETag"),recordVersion:u.headers.get("X-Dmp-Record-Version"),location:u.headers.get("Location"),replayed:u.headers.get("Idempotency-Replayed")==="true",rateLimit:Ne(u.headers)}};return{async capabilities(a={}){let l=(await i("GET","/_capabilities",E({},a))).data;return{...l,resources:l.resources.map(je)}},resources:a=>{if(!Ce.test(a))throw new TypeError(`'${a}' is not a resource name: cube/api names match [a-z][a-z0-9_]*.`);let l=`/${a}`,c=d=>{let u=String(d);if(u==="")throw new TypeError(`${a}: an empty id would address the collection, not a record.`);if(ie(u))throw new TypeError(`${a}: '${u}' is not an id: it would address another path, not a record.`);return`${l}/${encodeURIComponent(u)}`},g=d=>d===void 0?"":v(a,{fields:d}),b=async(d={},u={})=>await i("GET",l,E({query:v(a,d)},u)),w=async function*(d={},u={}){let f=d.page?.number??1;for(;;){let m=await b({...d,page:{...d.page,number:f}},u);yield m;let y=m.meta.page;if(y==null||y.number>=y.pages||m.data.length===0)return;f=y.number+1}},p=async(d,u,f)=>{let m=await i("POST",d,{query:g(f.fields),body:u??{},headers:U(f)});return{...m,data:m.status===204?null:m.data,accepted:m.status===202}};return{name:a,list:b,pages:w,async*all(d={},u={}){for await(let f of w(d,u))yield*f.data},get:(d,u={},f={})=>i("GET",c(d),E({query:v(a,u)},f)),aggregate:(d,u={})=>i("GET",`${l}/_aggregate`,E({query:v(a,d)},u)),create:(d,u={})=>i("POST",l,{query:g(u.fields),body:d,headers:U(u)}),update:(d,u,f={})=>i("PATCH",c(d),{query:g(f.fields),body:u,headers:U(f)}),delete:async(d,u={})=>({...await i("DELETE",c(d),{headers:U(u)}),data:null}),operation:(d,u,f,m={})=>p(`${c(d)}/${encodeURIComponent(te(u))}`,f,m),collectionOperation:(d,u,f={})=>p(`${l}/_operations/${encodeURIComponent(te(d))}`,u,f)}},request:i}}function je(e){return{...e,operations:(e.operations??[]).map(n=>({...n,input:q(n.input)})),aggregatable:q(e.aggregatable),relations:q(e.relations)}}function q(e){return Array.isArray(e)||e===void 0||e===null?{}:e}function te(e){if(e===""||e.startsWith("_"))throw new TypeError(`'${e}' is not an operation name: the '_' namespace is reserved by the API.`);if(ie(e))throw new TypeError(`'${e}' is not an operation name: it would address another path.`);return e}function ie(e){return e==="."||e===".."}function U(e){let n={};if(e.idempotencyKey!==void 0&&(n["Idempotency-Key"]=e.idempotencyKey),e.ifMatch!==void 0){if(e.ifMatch.startsWith("W/"))throw new TypeError("ifMatch takes recordVersion (X-Dmp-Record-Version), not the weak ETag.");n["If-Match"]=e.ifMatch}return n}function E(e,n){return n.signal===void 0?e:{...e,signal:n.signal}}async function Ie(e){let n=await e.text();if(n==="")return null;try{return JSON.parse(n)}catch{return n}}function Le(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)&&"data"in e}function Ne(e){let n=t=>{let r=e.get(t);return r===null||r===""||!Number.isFinite(Number(r))?null:Number(r)};return{limit:n("X-RateLimit-Limit"),remaining:n("X-RateLimit-Remaining"),reset:n("X-RateLimit-Reset")}}var se=/^[a-z][a-z0-9_]*$/,oe=["eq","neq","gt","gte","lt","lte","in","like","ilike","is","between"],ze=[[">=","gte"],["<=","lte"],["<>","neq"],[">","gt"],["<","lt"],["=","eq"]],Q=20,V=200,Fe=50,K=200,qe="1.0",C=class{constructor(n,t=null,r=()=>{}){this.manifest=t;this.guard=r;k(this,"tables",new Map);for(let[s,i]of Object.entries(Qe(n))){if(typeof i!="object"||i===null||Array.isArray(i))continue;let o=typeof i.schema?.name=="string"&&i.schema.name!==""?i.schema.name:s;se.test(o)&&this.tables.set(o,Ve(o,i))}}resourceNames(){return[...this.tables.keys()]}resource(n){if(!se.test(n))throw new TypeError(`'${n}' is not a resource name: cube/api names match [a-z][a-z0-9_]*.`);let t=(i,o)=>Promise.resolve().then(()=>o(this.authorize(n,i))),r=(i={})=>t("list",o=>Ge(o,i)),s=async function*(i={}){let o=i.page?.number??1;for(;;){let a=await r({...i,page:{...i.page,number:o}});if(yield a,a.meta.page.number>=a.meta.page.pages||a.data.length===0)return;o=a.meta.page.number+1}};return{name:n,list:r,pages:s,async*all(i={}){for await(let o of s(i))yield*o.data},get:(i,o={})=>t("get",a=>Je(a,i,o)),aggregate:i=>t("list",o=>Ze(o,i)),create:(i,o={})=>t("create",a=>tn(a,i,o)),update:(i,o,a={})=>t("update",l=>rn(l,i,o,a)),delete:(i,o={})=>t("delete",a=>sn(a,i,o)),operation:(i,o,a,l)=>t("get",c=>{throw I(c,i),le(o)}),collectionOperation:i=>t("list",()=>{throw le(i)})}}authorize(n,t){this.guard();let r=this.tables.get(n);if(r===void 0)throw h(404,"not_found",`Unknown API resource [${n}].`);let s=this.manifest?.uses;if(s!==void 0&&!(s[n]??[]).includes(t))throw h(403,"client_scope_exceeded",`This app may not ${t} '${n}': add it to "uses" in cube-app.json.`);return r}};function Qe(e){let n=e.resources;return Object.keys(e).length===1&&fe(n)&&!("rows"in n)&&!("schema"in n)?n:e}function Ve(e,n){let t=n.schema??{},r=(n.rows??[]).map(c=>J(c)),s=new Map,i=Array.isArray(t.fields)?t.fields.map(c=>[c.name,c]):Object.entries(t.fields??{});for(let[c,g]of i)s.set(c,Ke(c,g));for(let c of s.size===0?r:[])for(let g of Object.keys(c))s.has(g)||s.set(g,{name:g,types:[],nullable:!0});let o=[...s.keys()],a=new Set(["id",...t.readonly??[],...i.filter(([,c])=>c.readonly===!0).map(([c])=>c)]),l=c=>c===void 0?null:c.filter(g=>s.has(g));return{name:e,fields:s,filterable:l(t.filters)??o,writable:new Set(o.filter(c=>!a.has(c))),summary:(l(t.summary_fields)??[]).length>0?l(t.summary_fields):o,rows:r,versions:new Map(r.map(c=>[_(c.id),1])),replays:new Map}}function Ke(e,n){let t=n.type,r=typeof t=="string"?t.split("|"):Array.isArray(t)?[...t]:[];return{name:e,types:r.filter(s=>s!=="null"&&s!==""),nullable:n.nullable===!0||r.includes("null")}}function x(e,n){return new URLSearchParams(v(e.name,n))}function Ge(e,n){let t=x(e,n),r=j(e,t),s=ce(e,t),i=Be(e,t),{size:o,number:a}=Ye(t),l=ue(e,t),c=e.rows.filter(p=>s.every(d=>de(e,p,d)));i.length>0&&(c=[...c].sort((p,d)=>{for(let{field:u,descending:f}of i){let m=S(p[u],d[u]);if(m!==0)return f?-m:m}return 0}));let g=c.length,b=c.slice((a-1)*o,a*o).map(p=>L(p,r)),w={size:o,number:a,total:g,pages:Math.max(1,Math.ceil(g/o))};return{...D(200,b,O(e,w,l)),meta:{...O(e,w,l),page:w}}}function Je(e,n,t){let r=x(e,t),s=j(e,r),i=ue(e,r),o=I(e,n);return D(200,L(o,s),O(e,null,i),N(e,o))}function j(e,n){let t=`fields[${e.name}]`,r=n.get(t);if(r===null||r==="")return e.summary;let s=r.split(",").map(o=>o.trim()),i=s.filter(o=>!e.fields.has(o));if(i.length>0)throw h(422,"query_invalid",`Unknown ${t}: ${i.join(", ")}.`,{[t]:[`Allowed values: ${[...e.fields.keys()].join(", ")}.`]});return s}function ue(e,n){let t=r=>(n.get(r)??"").split(",").filter(s=>s!=="");return[...new Set([...t("include"),...t("with_count")])]}function ce(e,n){let t=[],r=[];n.forEach((s,i)=>r.push([i,s]));for(let[s,i]of r){let o=/^filter\[([^\]]+)\](?:\[([^\]]+)\])?$/.exec(s);if(o===null)continue;let a=o[1];if(!e.filterable.includes(a))throw h(422,"query_invalid",`Unknown filter: ${a}.`,{filter:[`Allowed values: ${e.filterable.join(", ")}.`]},"not_filterable");t.push(o[2]===void 0?We(a,i):He(a,o[2],i))}if(t.length>Q)throw h(422,"query_invalid",`A query takes at most ${Q} filters.`,{filter:[`Sent ${t.length}; the limit is ${Q}.`]});return t}function We(e,n){for(let[t,r]of ze)if(n.startsWith(t))return{field:e,operator:r,value:n.slice(t.length)};return{field:e,operator:"eq",value:n}}function He(e,n,t){if(!oe.includes(n))throw h(422,"query_invalid",`Unknown filter operator: ${n}.`,{filter:[`Allowed operators: ${oe.join(", ")}.`]});if(n==="between"&&t.split(",").length!==2)throw h(422,"query_invalid",`filter[${e}][between] takes exactly 2 values.`,{filter:[`Write filter[${e}][between]=low,high. Received ${t.split(",").length} value(s).`]});if(n==="in"&&t.split(",").length>V)throw h(422,"query_invalid",`filter[${e}][in] takes at most ${V} values.`,{filter:[`Sent ${t.split(",").length}; the limit is ${V}.`]});if(n==="is"&&t!=="null"&&t!=="not_null")throw h(422,"query_invalid",`filter[${e}][is] takes null or not_null.`,{filter:[`Received: ${t}.`]});return{field:e,operator:n,value:t}}function de(e,n,t){let r=n[t.field],s=i=>ln(e.fields.get(t.field),i.trim());switch(t.operator){case"eq":return G(r,s(t.value));case"neq":return r!=null&&!G(r,s(t.value));case"gt":return r!=null&&S(r,s(t.value))>0;case"gte":return r!=null&&S(r,s(t.value))>=0;case"lt":return r!=null&&S(r,s(t.value))<0;case"lte":return r!=null&&S(r,s(t.value))<=0;case"in":return t.value.split(",").some(i=>G(r,s(i)));case"between":{let[i,o]=t.value.split(",").map(s);return r!=null&&S(r,i)>=0&&S(r,o)<=0}case"like":case"ilike":return typeof r=="string"&&Xe(r,t.value);case"is":return t.value==="null"?r==null:r!=null;default:return!1}}function Xe(e,n){let t=n.split("%").map(r=>r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join(".*");return new RegExp(t,"iu").test(e)}function Be(e,n){let t=n.get("sort");if(t===null||t==="")return[];let r=t.split(",").map(i=>i.trim()).filter(i=>i!=="").map(i=>({field:i.replace(/^[-+]/,""),descending:i.startsWith("-")})),s=r.map(i=>i.field).filter(i=>!e.fields.has(i));if(s.length>0)throw h(422,"query_invalid",`Unknown sort: ${s.join(", ")}.`,{sort:[`Allowed values: ${[...e.fields.keys()].join(", ")}.`]},"not_sortable");return r}function Ye(e){let n=r=>{let s=e.get(r);if(s===null||s==="")return null;if(!/^\d+$/.test(s))throw h(422,"query_invalid",`${r} must be a positive integer.`,{[r]:["Expected a positive integer."]});return Number(s)},t=n("page[size]")??Fe;if(t>K)throw h(422,"cardinality_exceeded",`page[size] may not exceed ${K}.`,{"page.size":[`The maximum page size is ${K}.`]});return{size:Math.max(1,t),number:Math.max(1,n("page[number]")??1)}}function Ze(e,n){let t=x(e,n),r=ce(e,t),s=(t.get("fn")??"").split(",").filter(g=>g!==""),i=(t.get("group")??"").split(",").filter(g=>g!=="");if(s.length===0)throw h(422,"query_invalid","fn is required.",{fn:["Name at least one function, e.g. fn=count."]});let o=s.map(g=>en(e,g)),a=i.map(g=>nn(e,g)),l=new Map;for(let g of e.rows.filter(b=>r.every(w=>de(e,b,w)))){let b={};for(let[d,u]of i.entries())b[u]=a[d](g);let w=JSON.stringify(Object.values(b)),p=l.get(w)??{key:b,rows:[]};p.rows.push(g),l.set(w,p)}i.length===0&&l.size===0&&l.set("[]",{key:{},rows:[]});let c=[...l.values()].map(({key:g,rows:b})=>{let w={...g};for(let[p,d]of s.entries())w[d]=o[p](b);return w});return D(200,c,{...O(e,null,[]),groups:i.length===0?null:c.length})}function en(e,n){if(n==="count")return i=>i.length;let t=/^(count|sum|avg|min|max)\(([^)]+)\)$/.exec(n);if(t===null)throw h(422,"query_invalid",`Unknown function: ${n}.`,{fn:["Allowed: count, count(field), sum(field), avg(field), min(field), max(field)."]},"unknown_function");let[,r,s]=t;if(!e.fields.has(s))throw h(422,"query_invalid",`Unknown fn: ${s}.`,{fn:[`Allowed values: ${[...e.fields.keys()].join(", ")}.`]},"not_aggregatable");return i=>{let o=i.map(l=>l[s]).filter(l=>l!=null),a=o.map(Number).filter(l=>Number.isFinite(l));switch(r){case"count":return o.length;case"sum":return a.reduce((l,c)=>l+c,0);case"avg":return a.length===0?null:a.reduce((l,c)=>l+c,0)/a.length;case"min":return o.length===0?null:o.reduce((l,c)=>S(c,l)<0?c:l);default:return o.length===0?null:o.reduce((l,c)=>S(c,l)>0?c:l)}}}function nn(e,n){let[t,r]=n.split(".");if(!e.fields.has(t))throw h(422,"query_invalid",`Unknown group: ${t}.`,{group:[`Allowed values: ${[...e.fields.keys()].join(", ")}.`]},"not_groupable");let s={year:4,month:7,day:10};if(r===void 0)return o=>o[t]??null;let i=s[r];if(i===void 0)throw h(422,"query_invalid",`Unknown truncation: ${r}.`,{group:["Allowed truncations in the preview: year, month, day."]},"unknown_truncation");return o=>typeof o[t]=="string"?o[t].slice(0,i):null}function tn(e,n,t){let r=t.fields===void 0?e.summary:j(e,x(e,{fields:t.fields})),s=JSON.stringify([n,t.fields??null]);if(t.idempotencyKey!==void 0){let l=e.replays.get(t.idempotencyKey);if(l!==void 0){if(l.body!==s)throw h(422,"validation_failed","This Idempotency-Key was already used with a different request body. Use a new key, or resend the original body.");return{...l.response,replayed:!0}}}let i=pe(e,n),o={id:an(e),...i};for(let l of e.fields.keys())l in o||(o[l]=null);e.rows.push(o),e.versions.set(_(o.id),1);let a=D(201,L(o,r),O(e,null,[]),N(e,o));return t.idempotencyKey!==void 0&&e.replays.set(t.idempotencyKey,{body:s,response:a}),a}function rn(e,n,t,r){let s=r.fields===void 0?e.summary:j(e,x(e,{fields:r.fields})),i=I(e,n);return ge(e,i,r.ifMatch),Object.assign(i,pe(e,t)),e.versions.set(_(i.id),(e.versions.get(_(i.id))??1)+1),D(200,L(i,s),O(e,null,[]),N(e,i))}function sn(e,n,t){let r=I(e,n);return ge(e,r,t.ifMatch),e.rows.splice(e.rows.indexOf(r),1),e.versions.delete(_(r.id)),D(204,null,null)}function pe(e,n){let t=[...e.writable],r=t.length===0?"This resource declares no writable fields.":`Writable fields: ${t.join(", ")}.`;if(!fe(n)||Object.keys(n).length===0)throw h(422,"validation_failed","The request body must be a JSON object naming at least one writable field.",{body:[r]});let s=Object.keys(n).filter(o=>!e.writable.has(o));if(s.length>0)throw h(422,"query_invalid",`Unknown or read-only fields: ${s.join(", ")}.`,Object.fromEntries(s.map(o=>[o,[r]])));let i={};for(let[o,a]of Object.entries(n)){let l=on(e.fields.get(o),a);l!==null&&(i[o]=[l])}if(Object.keys(i).length>0)throw h(422,"validation_failed","The request body failed validation.",i);return J(n)}function on(e,n){return e===void 0||e.types.length!==1?null:n===null?e.nullable?null:ae(e):(()=>{switch(e.types[0]){case"string":return typeof n=="string";case"integer":return typeof n=="number"&&Number.isInteger(n)||typeof n=="string"&&/^[+-]?\d+$/.test(n.trim());case"number":return typeof n=="number"&&Number.isFinite(n)||typeof n=="string"&&n.trim()!==""&&Number.isFinite(Number(n));case"boolean":return[!0,!1,0,1,"0","1"].includes(n);case"array":case"object":return typeof n=="object";default:return!0}})()?null:ae(e)}function ae(e){return`The ${e.name.replace(/_/g," ")} field must be ${{string:"a string",integer:"an integer",number:"a number",boolean:"true or false",array:"an array",object:"an array"}[e.types[0]]??"valid"}.`}function ge(e,n,t){if(t===void 0)return;if(t.startsWith("W/"))throw new TypeError("ifMatch takes recordVersion (X-Dmp-Record-Version), not the weak ETag.");let r=N(e,n);if(t!==r)throw h(409,"conflict","The record changed since the version you sent.",void 0,void 0,{"X-Dmp-Record-Version":r})}function an(e){let n=e.rows.map(t=>t.id);return n.length>0&&n.every(t=>typeof t=="string")&&n.some(t=>!/^\d+$/.test(t))?crypto.randomUUID():n.reduce((t,r)=>Math.max(t,Number(r)||0),0)+1}function I(e,n){let t=_(n);if(t===""||t==="."||t==="..")throw new TypeError(`${e.name}: '${t}' is not an id.`);let r=e.rows.find(s=>_(s.id)===t);if(r===void 0)throw h(404,"not_found",`No such ${e.name} record.`);return r}function le(e){return h(404,"not_found",`No such operation [${e}]. The preview runs no custom operations.`)}function L(e,n){let t={};for(let r of n)t[r]=r in e?J(e[r]):null;return t}function N(e,n){return`"preview-${_(n.id)}-${e.versions.get(_(n.id))??1}"`}function O(e,n,t){return{resource:e.name,contract:qe,page:n,groups:null,omitted:t,pruned:[],forbidden_fields:[]}}function D(e,n,t,r=null){let s=new Headers({"X-Dmp-Preview":"true"});return r!==null&&s.set("X-Dmp-Record-Version",r),{status:e,data:n,meta:t,headers:s,etag:null,recordVersion:r,location:null,replayed:!1,rateLimit:{limit:null,remaining:null,reset:null}}}function h(e,n,t,r,s,i={}){return new R(e,new Headers({"Content-Type":"application/problem+json",...i}),{type:`https://docs.cube.nl/dmp-api/problems/${n}`,title:n.replace(/_/g," "),status:e,code:n,...s!==void 0?{reason:s}:{},detail:t,...r!==void 0?{errors:r}:{}})}function ln(e,n){let t=e?.types[0];if((t==="integer"||t==="number")&&n!==""&&Number.isFinite(Number(n)))return Number(n);if(t==="boolean"){if(n==="1"||n==="true")return!0;if(n==="0"||n==="false")return!1}return n}function G(e,n){return e==null?!1:typeof e=="boolean"&&typeof n!="boolean"?(e?"1":"0")===String(n)||String(e)===String(n):typeof e=="number"&&typeof n=="string"?String(e)===n:e===n||typeof e!="object"&&String(e)===String(n)}function S(e,n){if(e==null||n==null)return e==null?n==null?0:-1:1;if(typeof e=="number"&&typeof n=="number")return e-n;if(typeof e=="boolean"||typeof n=="boolean")return+(e===!0||e===1||e==="1")-+(n===!0||n===1||n==="1");let t=String(e),r=String(n);return t<r?-1:t>r?1:0}function _(e){return e==null?"":String(e)}function fe(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function J(e){return e===void 0?e:JSON.parse(JSON.stringify(e))}var me="1.0.0-rc.3";var cn="./cube-app.config.json",dn="./cube-mock.json",pn="./cube-app.json",gn={id:"preview",name:"Voorbeeldgebruiker",scopes:[]},ye=null;async function fn(e={}){let n=e.fetch??globalThis.fetch.bind(globalThis),t=e.pageUrl??Tn(),r=await W(n,z(cn,t));return r===null?mn(e,n,t):yn(bn(r),e,n,t)}async function mn(e,n,t){let r=e.mock??ye??await W(n,z(dn,t))??{},s=e.manifest===!1?null:e.manifest??await W(n,z(pn,t)),i=!0,o=new C(r,s,()=>{if(!i)throw new R(401,new Headers({"Content-Type":"application/problem+json"}),{type:"https://docs.cube.nl/dmp-api/problems/token_invalid",title:"token invalid",status:401,code:"token_invalid",detail:"Unauthenticated."})});return{mode:"preview",signInError:null,user:()=>i?{...gn}:null,async signIn(){i=!0},async signOut(){i=!1},resource:a=>o.resource(a)}}async function yn(e,n,t,r){let s=e.dmpUrl.replace(/\/+$/,""),i=null,o=kn(n.tokenStorage==="memory"?M():Rn(`cube-app.tokens.${e.clientId}`),g=>{i=g}),a=Z({issuer:e.issuer??s,clientId:e.clientId,redirectUri:z(e.redirectUri,r),scopes:e.scopes,fetch:t,tokenStore:o,...e.metadata!==void 0?{metadata:e.metadata}:{}}),l=re({baseUrl:e.apiUrl??`${s}/apps/api/v1`,auth:a,fetch:t}),c=null;i=await o.get();try{await a.handleCallback()}catch(g){c=g instanceof Error?g:new Error(String(g))}return i===null&&c===null&&n.autoSignIn===!0&&await a.signIn(),{mode:"live",get signInError(){return c},user:()=>i===null?null:hn(i),async signIn(){c=null,await a.signIn()},async signOut(){await a.signOut()},resource:g=>l.resources(g)}}function hn(e){let n=wn(e.accessToken),t=n?.sub,r=Array.isArray(n?.scopes)?n.scopes.map(String):null;return{id:typeof t=="string"||typeof t=="number"?String(t):null,name:null,scopes:r??(e.scope??"").split(" ").filter(s=>s!=="")}}function wn(e){let n=e.split(".")[1];if(n===void 0)return null;try{let t=n.replace(/-/g,"+").replace(/_/g,"/"),r=atob(t.padEnd(t.length+(4-t.length%4)%4,"=")),s=new TextDecoder().decode(Uint8Array.from(r,o=>o.charCodeAt(0))),i=JSON.parse(s);return i!==null&&typeof i=="object"?i:null}catch{return null}}function bn(e){let n=["dmpUrl","clientId","redirectUri"].filter(r=>typeof e[r]!="string"||e[r]===""),t=e.scopes;if(typeof t=="string"||Array.isArray(t)&&t.every(r=>typeof r=="string")||n.push("scopes"),n.length>0)throw new TypeError(`cube-app.config.json is incomplete: ${n.join(", ")} missing or not a string.`);return e}async function W(e,n){let t;try{t=await e(n,{headers:{Accept:"application/json"},...typeof document<"u"?{cache:"no-store"}:{}})}catch{return null}if(t.status===404||t.status===410)return null;if(!t.ok)throw new Error(`${n} answered ${t.status}.`);try{let r=JSON.parse(await t.text());return r!==null&&typeof r=="object"&&!Array.isArray(r)?r:null}catch{return null}}function kn(e,n){return{async get(){let t=await e.get();return n(t),t},async set(t){await e.set(t),n(t)}}}function Rn(e){let n=(()=>{try{return globalThis.sessionStorage??null}catch{return null}})();return n===null?M():{get(){try{let t=n.getItem(e);return t===null?null:JSON.parse(t)}catch{return null}},set(t){try{t===null?n.removeItem(e):n.setItem(e,JSON.stringify(t))}catch{}}}}function Tn(){let e=globalThis;return e.document?.baseURI??e.location?.href??"http://localhost/"}function z(e,n){return new URL(e,n).href}function Sn(e){ye=e}var he=Object.freeze({connect:fn,mock:Sn,version:me,sdk:"1"});globalThis.CubeApp=he;})();
|
|
1
|
+
/*! @cube-nl/dmp-sdk 1.0.0-rc.4 cube-sdk.js (window.CubeApp) | MIT */
|
|
2
|
+
"use strict";(()=>{var Ae=Object.defineProperty;var ve=(e,t,n)=>t in e?Ae(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var k=(e,t,n)=>ve(e,typeof t!="symbol"?t+"":t,n);var R=class extends Error{constructor(n,r,s){let i=s!==null&&typeof s=="object"?s:{},o=A(i.detail)??A(i.error_description);super(o??`HTTP ${n}`);k(this,"status");k(this,"code");k(this,"reason");k(this,"type");k(this,"title");k(this,"detail");k(this,"errors");k(this,"retryAfter");k(this,"recordVersion");k(this,"allow");k(this,"headers");k(this,"body");this.name="DmpError",this.status=n,this.code=A(i.code)??A(i.error),this.reason=A(i.reason),this.type=A(i.type),this.title=A(i.title),this.detail=o,this.errors=$e(i.errors)?i.errors:null,this.retryAfter=De(r.get("Retry-After")),this.recordVersion=r.get("X-Dmp-Record-Version"),this.allow=r.has("Allow")?(r.get("Allow")??"").split(",").map(a=>a.trim()).filter(a=>a!==""):null,this.headers=r,this.body=s}get isProblem(){return this.code!==null}};function A(e){return typeof e=="string"?e:null}function $e(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function De(e){if(e===null||e.trim()==="")return null;let t=Number(e);if(Number.isFinite(t))return t;let n=Date.parse(e);return Number.isNaN(n)?null:Math.max(0,Math.ceil((n-Date.now())/1e3))}function te(e){return{async getAccessToken(){let t=typeof e=="function"?await e():e;return t===void 0||t===""?null:t}}}var U="cube-dmp-sdk.pending-login";function re(e){let t=e.fetch??globalThis.fetch.bind(globalThis),n=e.tokenStore??M(),r=e.pendingStore??Ue(),s=(e.refreshLeewaySeconds??30)*1e3,i=typeof e.scopes=="string"?e.scopes:e.scopes.join(" "),o=e.metadata??{},a=Oe(o)?Promise.resolve({...o,issuer:o.issuer??e.issuer}):null,p=null,d=()=>(a??(a=xe(e.issuer,t).then(c=>({...c,...o}),c=>{throw a=null,c})),a),m=async(c,u)=>{let{token_endpoint:l}=await d(),f=new URLSearchParams(c);e.clientSecret!==void 0&&f.set("client_secret",e.clientSecret);let y=await t(l,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/x-www-form-urlencoded"},body:f.toString()}),b=await ie(y);if(!y.ok||b===null||typeof b!="object"||typeof b.access_token!="string")throw new R(y.status,y.headers,b);let T={accessToken:b.access_token,refreshToken:typeof b.refresh_token=="string"?b.refresh_token:null,expiresAt:typeof b.expires_in=="number"?Date.now()+b.expires_in*1e3:null,scope:typeof b.scope=="string"?b.scope:u};return await n.set(T),T},g=()=>(p??(p=(async()=>{let c=await n.get();if(c?.refreshToken==null)throw new TypeError("No refresh token: sign in again.");try{return await m({grant_type:"refresh_token",refresh_token:c.refreshToken,client_id:e.clientId},c.scope)}catch(u){throw u instanceof R&&u.status>=400&&u.status<500&&await n.set(null),u}})().finally(()=>{p=null})),p),h=async(c={})=>{let u=await d();if(!(u.code_challenge_methods_supported??[]).includes("S256"))throw new TypeError("The authorization server does not offer PKCE S256; this SDK will not sign in without it.");let l=ne(64),f=c.state??ne(32),y=se(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(l))),b={verifier:l,state:f,redirectUri:e.redirectUri,clientId:e.clientId};r.setItem(U,JSON.stringify(b));let T=new URL(u.authorization_endpoint);return T.search=new URLSearchParams({...c.extraParams,response_type:"code",client_id:e.clientId,redirect_uri:e.redirectUri,scope:i,state:f,code_challenge:y,code_challenge_method:"S256"}).toString(),T.toString()};return{discover:d,authorizationUrl:h,async signIn(c={}){let u=ee();if(u===null)throw new TypeError("signIn() needs a browser; use authorizationUrl() and redirect yourself.");u.assign(await h(c))},async handleCallback(c){let u=c??ee()?.href;if(u===void 0)throw new TypeError("handleCallback() needs the callback URL outside a browser.");let l=new URL(u),f=new URLSearchParams(l.search);if(!Me(l,e.redirectUri)||!f.has("code")&&!f.has("error"))return null;if(c===void 0){for(let b of["code","state","error","error_description","iss"])l.searchParams.delete(b);globalThis.history?.replaceState(null,"",l.pathname+l.search+l.hash)}let y=Ee(r.getItem(U));if(y===null||f.get("state")!==y.state)throw new TypeError("The state does not match: this callback does not belong to a sign-in started here.");if(r.removeItem(U),f.has("error"))throw new R(400,new Headers,{error:f.get("error")??"access_denied",error_description:f.get("error_description")??void 0});return m({grant_type:"authorization_code",code:f.get("code")??"",redirect_uri:y.redirectUri,client_id:y.clientId,code_verifier:y.verifier},i)},refresh:g,async getTokens(){return n.get()},async setTokens(c){await n.set(c)},async signOut(){await n.set(null),r.removeItem(U)},async getAccessToken(){let c=await n.get();if(c===null)return null;if(c.expiresAt!==null&&c.expiresAt-s<=Date.now()&&c.refreshToken!==null)try{return(await g()).accessToken}catch{return(await n.get())?.accessToken??null}return c.accessToken},async onUnauthorized(){if((await n.get())?.refreshToken==null)return!1;try{return await g(),!0}catch{return!1}}}}function Oe(e){return typeof e.authorization_endpoint=="string"&&typeof e.token_endpoint=="string"}async function xe(e,t=globalThis.fetch.bind(globalThis)){let n=`${e.replace(/\/+$/,"")}/.well-known/oauth-authorization-server`,r=await t(n,{headers:{Accept:"application/json"}}),s=await ie(r);if(!r.ok||s===null||typeof s!="object")throw new R(r.status,r.headers,s);return s}function M(e=null){let t=e;return{get:()=>t,set:n=>{t=n}}}function Pe(){let e=new Map;return{getItem:t=>e.get(t)??null,setItem:(t,n)=>{e.set(t,n)},removeItem:t=>{e.delete(t)}}}function Ue(){try{let e=globalThis.sessionStorage;if(e!==void 0)return e}catch{}return Pe()}function ee(){let e=globalThis.location;return e!==void 0&&typeof e.assign=="function"?e:null}function Me(e,t){let n=new URL(t),r=s=>s.replace(/\/+$/,"")||"/";return e.origin===n.origin&&r(e.pathname)===r(n.pathname)}function Ee(e){if(e===null)return null;try{let t=JSON.parse(e);return typeof t.verifier=="string"&&typeof t.state=="string"&&typeof t.redirectUri=="string"&&typeof t.clientId=="string"?t:null}catch{return null}}async function ie(e){let t=await e.text();if(t==="")return null;try{return JSON.parse(t)}catch{return t}}function ne(e){return se(crypto.getRandomValues(new Uint8Array(e)))}function se(e){let t=e instanceof Uint8Array?e:new Uint8Array(e),n="";for(let r of t)n+=String.fromCharCode(r);return btoa(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}var Ce=new Set(["in","between"]);function v(e,t={}){let n=new URLSearchParams;je(n,e,t.fields),Ie(n,t.filter),O(n,"sort",t.sort),O(n,"include",t.include),O(n,"with_count",t.withCount),Ne(n,t.has),O(n,"fn",t.fn),O(n,"group",t.group),t.page?.size!==void 0&&n.append("page[size]",String(t.page.size)),t.page?.number!==void 0&&n.append("page[number]",String(t.page.number));for(let[r,s]of Object.entries(t.params??{}))n.append(r,s);return n.toString().replace(/%5B/gi,"[").replace(/%5D/gi,"]").replace(/%2C/gi,",").replace(/%28/gi,"(").replace(/%29/gi,")")}function je(e,t,n){if(n===void 0)return;let r=ze(n)?{[t]:n}:n;for(let[s,i]of Object.entries(r))i.length>0&&e.append(`fields[${s}]`,i.join(","))}function Ie(e,t){for(let[n,r]of Object.entries(t??{})){if(Array.isArray(r))throw new TypeError(`filter.${n}: a bare list names no operator and cube/api refuses it. Write { in: [...] } (or { between: [from, to] }).`);if(r==null)throw new TypeError(`filter.${n}: write { is: 'null' } or { is: 'not_null' } to filter on null.`);if(typeof r!="object"){e.append(`filter[${n}]`,q(r));continue}for(let[s,i]of Object.entries(r))i!==void 0&&e.append(`filter[${n}][${s}]`,Le(n,s,i))}}function Le(e,t,n){if(n===null){if(t!=="is")throw new TypeError(`filter.${e}.${t}: null is only meaningful under 'is'.`);return"null"}if(Array.isArray(n)){if(!Ce.has(t))throw new TypeError(`filter.${e}.${t}: takes one value; only 'in' and 'between' take a list.`);return n.map(q).join(",")}return q(n)}function Ne(e,t){for(let[n,r]of Object.entries(t??{}))r===!0?e.append(`has[${n}]`,"1"):typeof r=="number"?e.append(`has[${n}][min]`,String(r)):(r.min!==void 0&&e.append(`has[${n}][min]`,String(r.min)),r.max!==void 0&&e.append(`has[${n}][max]`,String(r.max)))}function O(e,t,n){if(n===void 0)return;let r=typeof n=="string"?n:n.filter(s=>s!=="").join(",");r!==""&&e.append(t,r)}function q(e){return typeof e=="boolean"?e?"1":"0":String(e)}function ze(e){return Array.isArray(e)}var Fe=/^[a-z][a-z0-9_]*$/;function ae(e){let t=e.baseUrl.replace(/\/+$/,""),n=e.fetch??globalThis.fetch.bind(globalThis),r=e.auth===void 0?null:typeof e.auth=="string"||typeof e.auth=="function"?te(e.auth):e.auth,s=e.cache??(typeof document<"u"?"no-cache":void 0),i=async(a,p,{query:d,body:m,headers:g={},signal:h}={})=>{let c=`${t}${p.startsWith("/")?p:`/${p}`}${d?`?${d}`:""}`,u=async()=>{let b=r===null?null:await r.getAccessToken(),T={method:a,headers:{Accept:"application/json",...e.headers,...b!==null?{Authorization:`Bearer ${b}`}:{},...m!==void 0?{"Content-Type":"application/json"}:{},...g}};m!==void 0&&(T.body=JSON.stringify(m)),s!==void 0&&(T.cache=s),h!==void 0&&(T.signal=h);let Se=Date.now(),Z=await n(c,T);return e.onResponse?.({method:a,url:c,status:Z.status,ms:Date.now()-Se}),Z},l=await u();l.status===401&&r?.onUnauthorized!==void 0&&await r.onUnauthorized()&&(await l.body?.cancel().catch(()=>{}),l=await u());let f=await Qe(l);if(!l.ok)throw new R(l.status,l.headers,f);let y=Ve(f)?f:null;return{status:l.status,data:y!==null?y.data:f,meta:y?.meta??null,headers:l.headers,etag:l.headers.get("ETag"),recordVersion:l.headers.get("X-Dmp-Record-Version"),location:l.headers.get("Location"),replayed:l.headers.get("Idempotency-Replayed")==="true",rateLimit:Ke(l.headers)}};return{async capabilities(a={}){let p=(await i("GET","/_capabilities",C({},a))).data;return{...p,resources:p.resources.map(qe)}},resources:a=>{if(!Fe.test(a))throw new TypeError(`'${a}' is not a resource name: cube/api names match [a-z][a-z0-9_]*.`);let p=`/${a}`,d=u=>{let l=String(u);if(l==="")throw new TypeError(`${a}: an empty id would address the collection, not a record.`);if(le(l))throw new TypeError(`${a}: '${l}' is not an id: it would address another path, not a record.`);return`${p}/${encodeURIComponent(l)}`},m=u=>u===void 0?"":v(a,{fields:u}),g=async(u={},l={})=>await i("GET",p,C({query:v(a,u)},l)),h=async function*(u={},l={}){let f=u.page?.number??1;for(;;){let y=await g({...u,page:{...u.page,number:f}},l);yield y;let b=y.meta.page;if(b==null||b.number>=b.pages||y.data.length===0)return;f=b.number+1}},c=async(u,l,f)=>{let y=await i("POST",u,{query:m(f.fields),body:l??{},headers:E(f)});return{...y,data:y.status===204?null:y.data,accepted:y.status===202}};return{name:a,list:g,pages:h,async*all(u={},l={}){for await(let f of h(u,l))yield*f.data},get:(u,l={},f={})=>i("GET",d(u),C({query:v(a,l)},f)),aggregate:(u,l={})=>i("GET",`${p}/_aggregate`,C({query:v(a,u)},l)),create:(u,l={})=>i("POST",p,{query:m(l.fields),body:u,headers:E(l)}),update:(u,l,f={})=>i("PATCH",d(u),{query:m(f.fields),body:l,headers:E(f)}),delete:async(u,l={})=>({...await i("DELETE",d(u),{headers:E(l)}),data:null}),operation:(u,l,f,y={})=>c(`${d(u)}/${encodeURIComponent(oe(l))}`,f,y),collectionOperation:(u,l,f={})=>c(`${p}/_operations/${encodeURIComponent(oe(u))}`,l,f)}},request:i}}function qe(e){return{...e,operations:(e.operations??[]).map(t=>({...t,input:Q(t.input)})),aggregatable:Q(e.aggregatable),relations:Q(e.relations)}}function Q(e){return Array.isArray(e)||e===void 0||e===null?{}:e}function oe(e){if(e===""||e.startsWith("_"))throw new TypeError(`'${e}' is not an operation name: the '_' namespace is reserved by the API.`);if(le(e))throw new TypeError(`'${e}' is not an operation name: it would address another path.`);return e}function le(e){return e==="."||e===".."}function E(e){let t={};if(e.idempotencyKey!==void 0&&(t["Idempotency-Key"]=e.idempotencyKey),e.ifMatch!==void 0){if(e.ifMatch.startsWith("W/"))throw new TypeError("ifMatch takes recordVersion (X-Dmp-Record-Version), not the weak ETag.");t["If-Match"]=e.ifMatch}return t}function C(e,t){return t.signal===void 0?e:{...e,signal:t.signal}}async function Qe(e){let t=await e.text();if(t==="")return null;try{return JSON.parse(t)}catch{return t}}function Ve(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)&&"data"in e}function Ke(e){let t=n=>{let r=e.get(n);return r===null||r===""||!Number.isFinite(Number(r))?null:Number(r)};return{limit:t("X-RateLimit-Limit"),remaining:t("X-RateLimit-Remaining"),reset:t("X-RateLimit-Reset")}}var ue=/^[a-z][a-z0-9_]*$/,ce=["eq","neq","gt","gte","lt","lte","in","like","ilike","is","between"],Ge=[[">=","gte"],["<=","lte"],["<>","neq"],[">","gt"],["<","lt"],["=","eq"]],H=["count","sum","avg","min","max"],de=["day","week","month","quarter","year"],V=20,K=5,ge=1e3,G=200,We=50,W=200,Je="1.0",j=class{constructor(t,n=null,r=()=>{}){k(this,"manifest",n);k(this,"guard",r);k(this,"tables",new Map);for(let[s,i]of Object.entries(He(t))){if(typeof i!="object"||i===null||Array.isArray(i))continue;let o=typeof i.schema?.name=="string"&&i.schema.name!==""?i.schema.name:s;ue.test(o)&&this.tables.set(o,Xe(o,i))}}resourceNames(){return[...this.tables.keys()]}resource(t){if(!ue.test(t))throw new TypeError(`'${t}' is not a resource name: cube/api names match [a-z][a-z0-9_]*.`);let n=(i,o)=>Promise.resolve().then(()=>o(this.authorize(t,i))),r=(i={})=>n("list",o=>Ye(o,i)),s=async function*(i={}){let o=i.page?.number??1;for(;;){let a=await r({...i,page:{...i.page,number:o}});if(yield a,a.meta.page.number>=a.meta.page.pages||a.data.length===0)return;o=a.meta.page.number+1}};return{name:t,list:r,pages:s,async*all(i={}){for await(let o of s(i))yield*o.data},get:(i,o={})=>n("get",a=>Ze(a,i,o)),aggregate:i=>n("aggregate",o=>on(o,i)),create:(i,o={})=>n("create",a=>cn(a,i,o)),update:(i,o,a={})=>n("update",p=>dn(p,i,o,a)),delete:(i,o={})=>n("delete",a=>gn(a,i,o)),operation:(i,o,a,p)=>n("get",d=>{throw L(d,i),me(o)}),collectionOperation:i=>n("list",()=>{throw me(i)})}}authorize(t,n){this.guard();let r=this.tables.get(t);if(r===void 0)throw w(404,"not_found",`Unknown API resource [${t}].`);let s=this.manifest?.uses;if(s!==void 0&&!(s[t]??[]).includes(n))throw w(403,"client_scope_exceeded",`This app may not ${n} '${t}': add it to "uses" in cube-app.json.`);return r}};function He(e){let t=e.resources;return Object.keys(e).length===1&&x(t)&&!("rows"in t)&&!("schema"in t)?t:e}function Xe(e,t){let n=t.schema??{},r=(t.rows??[]).map(g=>B(g)),s=new Map,i=Array.isArray(n.fields)?n.fields.map(g=>[g.name,g]):Object.entries(n.fields??{});for(let[g,h]of i)s.set(g,Be(g,h));for(let g of s.size===0?r:[])for(let h of Object.keys(g))s.has(h)||s.set(h,{name:h,types:[],nullable:!0});let o=[...s.keys()],a=(x(n.choices)?Object.entries(n.choices):[]).filter(g=>x(g[1])&&Array.isArray(g[1].values)),p=new Set(["id",...n.readonly??[],...i.filter(([,g])=>g.readonly===!0).map(([g])=>g),...a.filter(([,g])=>g.writable===!1).map(([g])=>g)]),d=new Map;for(let[g,h]of i)Array.isArray(h.enum)&&d.set(g,h.enum);for(let[g,h]of a)d.set(g,h.values);let m=g=>Array.isArray(g)?g.filter(h=>typeof h=="string"&&s.has(h)):null;return{name:e,fields:s,filterable:m(n.filters)??o,groupable:m(n.groupable)??o,aggregatable:new Map(x(n.aggregatable)?Object.entries(n.aggregatable).filter(([g,h])=>s.has(g)&&Array.isArray(h)).map(([g,h])=>[g,[...h]]):o.map(g=>[g,[...H]])),choices:d,writable:new Set(o.filter(g=>!p.has(g))),summary:(m(n.summary_fields)??[]).length>0?m(n.summary_fields):o,rows:r,versions:new Map(r.map(g=>[S(g.id),1])),replays:new Map}}function Be(e,t){let n=t.type,r=typeof n=="string"?n.split("|"):Array.isArray(n)?[...n]:[];return{name:e,types:r.filter(s=>s!=="null"&&s!==""),nullable:t.nullable===!0||r.includes("null")}}function P(e,t){return new URLSearchParams(v(e.name,t))}function Ye(e,t){let n=P(e,t),r=I(e,n),s=he(e,n),i=rn(e,n),{size:o,number:a}=sn(n),p=ye(e,n),d=e.rows.filter(c=>s.every(u=>we(e,c,u)));i.length>0&&(d=[...d].sort((c,u)=>{for(let{field:l,descending:f}of i){let y=_(c[l],u[l]);if(y!==0)return f?-y:y}return 0}));let m=d.length,g=d.slice((a-1)*o,a*o).map(c=>N(c,r)),h={size:o,number:a,total:m,pages:Math.max(1,Math.ceil(m/o))};return{...D(200,g,$(e,h,p)),meta:{...$(e,h,p),page:h}}}function Ze(e,t,n){let r=P(e,n),s=I(e,r),i=ye(e,r),o=L(e,t);return D(200,N(o,s),$(e,null,i),z(e,o))}function I(e,t){let n=`fields[${e.name}]`,r=t.get(n);if(r===null||r==="")return e.summary;let s=r.split(",").map(o=>o.trim()),i=s.filter(o=>!e.fields.has(o));if(i.length>0)throw w(422,"query_invalid",`Unknown ${n}: ${i.join(", ")}.`,{[n]:[`Allowed values: ${[...e.fields.keys()].join(", ")}.`]});return s}function ye(e,t){let n=r=>(t.get(r)??"").split(",").filter(s=>s!=="");return[...new Set([...n("include"),...n("with_count")])]}function he(e,t){let n=[],r=[];t.forEach((s,i)=>r.push([i,s]));for(let[s,i]of r){let o=/^filter\[([^\]]+)\](?:\[([^\]]+)\])?$/.exec(s);if(o===null)continue;let a=o[1];if(!e.filterable.includes(a))throw w(422,"query_invalid",`Unknown filter: ${a}.`,{filter:[`Allowed values: ${e.filterable.join(", ")}.`]},"not_filterable");n.push(o[2]===void 0?en(a,i):nn(a,o[2],i))}if(n.length>V)throw w(422,"query_invalid",`A query takes at most ${V} filters.`,{filter:[`Sent ${n.length}; the limit is ${V}.`]});return n}function en(e,t){for(let[n,r]of Ge)if(t.startsWith(n))return{field:e,operator:r,value:t.slice(n.length)};return{field:e,operator:"eq",value:t}}function nn(e,t,n){if(!ce.includes(t))throw w(422,"query_invalid",`Unknown filter operator: ${t}.`,{filter:[`Allowed operators: ${ce.join(", ")}.`]});if(t==="between"&&n.split(",").length!==2)throw w(422,"query_invalid",`filter[${e}][between] takes exactly 2 values.`,{filter:[`Write filter[${e}][between]=low,high. Received ${n.split(",").length} value(s).`]});if(t==="in"&&n.split(",").length>G)throw w(422,"query_invalid",`filter[${e}][in] takes at most ${G} values.`,{filter:[`Sent ${n.split(",").length}; the limit is ${G}.`]});if(t==="is"&&n!=="null"&&n!=="not_null")throw w(422,"query_invalid",`filter[${e}][is] takes null or not_null.`,{filter:[`Received: ${n}.`]});return{field:e,operator:t,value:n}}function we(e,t,n){let r=t[n.field],s=i=>yn(e.fields.get(n.field),i.trim());switch(n.operator){case"eq":return J(r,s(n.value));case"neq":return r!=null&&!J(r,s(n.value));case"gt":return r!=null&&_(r,s(n.value))>0;case"gte":return r!=null&&_(r,s(n.value))>=0;case"lt":return r!=null&&_(r,s(n.value))<0;case"lte":return r!=null&&_(r,s(n.value))<=0;case"in":return n.value.split(",").some(i=>J(r,s(i)));case"between":{let[i,o]=n.value.split(",").map(s);return r!=null&&_(r,i)>=0&&_(r,o)<=0}case"like":case"ilike":return typeof r=="string"&&tn(r,n.value);case"is":return n.value==="null"?r==null:r!=null;default:return!1}}function tn(e,t){let n=t.split("%").map(r=>r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join(".*");return new RegExp(n,"iu").test(e)}function rn(e,t){let n=t.get("sort");if(n===null||n==="")return[];let r=n.split(",").map(i=>i.trim()).filter(i=>i!=="").map(i=>({field:i.replace(/^[-+]/,""),descending:i.startsWith("-")})),s=r.map(i=>i.field).filter(i=>!e.fields.has(i));if(s.length>0)throw w(422,"query_invalid",`Unknown sort: ${s.join(", ")}.`,{sort:[`Allowed values: ${[...e.fields.keys()].join(", ")}.`]},"not_sortable");return r}function sn(e){let t=r=>{let s=e.get(r);if(s===null||s==="")return null;if(!/^\d+$/.test(s))throw w(422,"query_invalid",`${r} must be a positive integer.`,{[r]:["Expected a positive integer."]});return Number(s)},n=t("page[size]")??We;if(n>W)throw w(422,"cardinality_exceeded",`page[size] may not exceed ${W}.`,{"page.size":[`The maximum page size is ${W}.`]});return{size:Math.max(1,n),number:Math.max(1,t("page[number]")??1)}}function on(e,t){let n=P(e,t),r=(n.get("fn")??"").trim();if(r==="")throw w(422,"query_invalid","fn is required: name at least one aggregation function.",{fn:[`Expected e.g. fn=count or fn=sum(field). ${X(e)}`]});let s=r.split(",").map(c=>c.trim()),i=s.map(c=>an(e,c));pe(s,"fn");let o=(n.get("group")??"").trim(),a=o===""?[]:o.split(",").map(c=>c.trim());if(a.length>K)throw w(422,"cardinality_exceeded",`group may not exceed ${K} dimensions, ${a.length} given.`,{group:[`Use at most ${K} dimensions.`]});let p=a.map(c=>ln(e,c));pe(a,"group");let d=he(e,n),m=new Map;for(let c of e.rows.filter(u=>d.every(l=>we(e,u,l)))){let u={};for(let[y,b]of a.entries())u[b]=p[y](c);let l=JSON.stringify(Object.values(u)),f=m.get(l)??{key:u,rows:[]};f.rows.push(c),m.set(l,f)}if(a.length===0&&m.size===0&&m.set("[]",{key:{},rows:[]}),m.size>ge)throw w(422,"cardinality_exceeded","The grouping produces more groups than the configured maximum.",{groepen:["The grouping produces more groups than the configured maximum.",`Allowed values: groepeer grover, of filter eerst; maximaal ${ge} groepen.`]},"too_many_groups");let h=[...m.values()].sort((c,u)=>{for(let l of a){let f=_(c.key[l],u.key[l]);if(f!==0)return f}return 0}).map(({key:c,rows:u})=>{let l={...c};for(let[f,y]of s.entries())l[y]=i[f](u);return l});return D(200,h,{...$(e,null,[]),groups:h.length})}function pe(e,t){let n=[...new Set(e.filter((r,s)=>e.indexOf(r)!==s))];if(n.length>0)throw w(422,"query_invalid",`Duplicate ${t} entry: ${n.join(", ")}.`,{[t]:[`List each ${t} entry once.`]})}function X(e){return e.aggregatable.size===0?"This resource has no aggregatable fields; only bare count is available.":`Aggregatable fields: ${[...e.aggregatable.keys()].join(", ")}.`}function an(e,t){let n=/^([a-z]+)\(([a-z0-9_]+)\)$/.exec(t);if(n===null&&!/^[a-z]+$/.test(t))throw w(422,"query_invalid",`Unparseable fn entry: ${t}.`,{fn:["Expected `count` or `function(field)`, e.g. sum(field)."]});let r=n===null?t:n[1],s=n===null?null:n[2];if(!H.includes(r))throw w(422,"query_invalid",`Unknown aggregation function: ${r}.`,{fn:[`Allowed values: ${H.join(", ")}.`]});if(s===null){if(r!=="count")throw w(422,"query_invalid",`${r} must be applied to a field, e.g. ${r}(field).`,{fn:[X(e)]});return o=>o.length}let i=e.aggregatable.get(s);if(i===void 0)throw w(422,"query_invalid",`Unknown aggregatable field: ${s}.`,{fn:[X(e)]});if(!i.includes(r))throw w(422,"query_invalid",`${r} is not allowed on ${s}.`,{fn:[`Allowed functions for ${s}: ${i.join(", ")}.`]});return o=>{let a=o.map(d=>d[s]).filter(d=>d!=null),p=a.map(Number).filter(d=>Number.isFinite(d));switch(r){case"count":return o.length;case"sum":return p.length===0?null:p.reduce((d,m)=>d+m,0);case"avg":return p.length===0?null:p.reduce((d,m)=>d+m,0)/p.length;case"min":return a.length===0?null:a.reduce((d,m)=>_(m,d)<0?m:d);default:return a.length===0?null:a.reduce((d,m)=>_(m,d)>0?m:d)}}}function ln(e,t){let n=t.split(".");if(n.length>2)throw w(422,"query_invalid",`Unparseable group entry: ${t}.`,{group:["Expected `field` or `field.unit`, e.g. published_at.month."]});let[r,s]=n;if(!e.groupable.includes(r))throw w(422,"query_invalid",`Unknown group: ${r}.`,{group:[`Allowed values: ${e.groupable.join(", ")}.`]},"not_groupable");if(s===void 0)return i=>i[r]??null;if(!de.includes(s))throw w(422,"query_invalid",`Unknown truncation unit: ${s}.`,{group:[`Allowed values: ${de.join(", ")}.`]});return i=>un(i[r],s)}function un(e,t){let n=typeof e=="string"?/^(\d{4})-(\d{2})-(\d{2})/.exec(e):null;if(n===null)return null;let[r,s,i]=[Number(n[1]),Number(n[2]),Number(n[3])],o=new Date(Date.UTC(r,s-1,i));if(o.getUTCFullYear()!==r||o.getUTCMonth()!==s-1||o.getUTCDate()!==i)return null;switch(t){case"day":return n[0];case"week":{let a=new Date(o.getTime()+(3-(o.getUTCDay()+6)%7)*864e5),p=a.getUTCFullYear(),d=1+Math.floor((a.getTime()-Date.UTC(p,0,1))/(7*864e5));return`${p}-W${String(d).padStart(2,"0")}`}case"month":return n[0].slice(0,7);case"quarter":return`${n[1]}-Q${Math.ceil(s/3)}`;case"year":return n[1];default:return null}}function cn(e,t,n){let r=n.fields===void 0?e.summary:I(e,P(e,{fields:n.fields})),s=JSON.stringify([t,n.fields??null]);if(n.idempotencyKey!==void 0){let p=e.replays.get(n.idempotencyKey);if(p!==void 0){if(p.body!==s)throw w(422,"validation_failed","This Idempotency-Key was already used with a different request body. Use a new key, or resend the original body.");return{...p.response,replayed:!0}}}let i=be(e,t),o={id:mn(e),...i};for(let p of e.fields.keys())p in o||(o[p]=null);e.rows.push(o),e.versions.set(S(o.id),1);let a=D(201,N(o,r),$(e,null,[]),z(e,o));return n.idempotencyKey!==void 0&&e.replays.set(n.idempotencyKey,{body:s,response:a}),a}function dn(e,t,n,r){let s=r.fields===void 0?e.summary:I(e,P(e,{fields:r.fields})),i=L(e,t);return ke(e,i,r.ifMatch),Object.assign(i,be(e,n)),e.versions.set(S(i.id),(e.versions.get(S(i.id))??1)+1),D(200,N(i,s),$(e,null,[]),z(e,i))}function gn(e,t,n){let r=L(e,t);return ke(e,r,n.ifMatch),e.rows.splice(e.rows.indexOf(r),1),e.versions.delete(S(r.id)),D(204,null,null)}function be(e,t){let n=[...e.writable],r=n.length===0?"This resource declares no writable fields.":`Writable fields: ${n.join(", ")}.`;if(!x(t)||Object.keys(t).length===0)throw w(422,"validation_failed","The request body must be a JSON object naming at least one writable field.",{body:[r]});let s=Object.keys(t).filter(o=>!e.writable.has(o));if(s.length>0)throw w(422,"query_invalid",`Unknown or read-only fields: ${s.join(", ")}.`,Object.fromEntries(s.map(o=>[o,[r]])));let i={};for(let[o,a]of Object.entries(t)){let p=e.fields.get(o),d=fn(p,a)??pn(e,p,o,a);d!==null&&(i[o]=[d])}if(Object.keys(i).length>0)throw w(422,"validation_failed","The request body failed validation.",i);return B(t)}function pn(e,t,n,r){let s=e.choices.get(n);return s===void 0||r===null&&t?.nullable===!0||s.includes(r)?null:`The selected ${n.replace(/_/g," ")} is invalid.`}function fn(e,t){return e===void 0||e.types.length!==1?null:t===null?e.nullable?null:fe(e):(()=>{switch(e.types[0]){case"string":return typeof t=="string";case"integer":return typeof t=="number"&&Number.isInteger(t)||typeof t=="string"&&/^[+-]?\d+$/.test(t.trim());case"number":return typeof t=="number"&&Number.isFinite(t)||typeof t=="string"&&t.trim()!==""&&Number.isFinite(Number(t));case"boolean":return[!0,!1,0,1,"0","1"].includes(t);case"array":case"object":return typeof t=="object";default:return!0}})()?null:fe(e)}function fe(e){return`The ${e.name.replace(/_/g," ")} field must be ${{string:"a string",integer:"an integer",number:"a number",boolean:"true or false",array:"an array",object:"an array"}[e.types[0]]??"valid"}.`}function ke(e,t,n){if(n===void 0)return;if(n.startsWith("W/"))throw new TypeError("ifMatch takes recordVersion (X-Dmp-Record-Version), not the weak ETag.");let r=z(e,t);if(n!==r)throw w(409,"conflict","The record changed since the version you sent.",void 0,void 0,{"X-Dmp-Record-Version":r})}function mn(e){let t=e.rows.map(n=>n.id);return t.length>0&&t.every(n=>typeof n=="string")&&t.some(n=>!/^\d+$/.test(n))?crypto.randomUUID():t.reduce((n,r)=>Math.max(n,Number(r)||0),0)+1}function L(e,t){let n=S(t);if(n===""||n==="."||n==="..")throw new TypeError(`${e.name}: '${n}' is not an id.`);let r=e.rows.find(s=>S(s.id)===n);if(r===void 0)throw w(404,"not_found",`No such ${e.name} record.`);return r}function me(e){return w(404,"not_found",`No such operation [${e}]. The preview runs no custom operations.`)}function N(e,t){let n={};for(let r of t)n[r]=r in e?B(e[r]):null;return n}function z(e,t){return`"preview-${S(t.id)}-${e.versions.get(S(t.id))??1}"`}function $(e,t,n){return{resource:e.name,contract:Je,page:t,groups:null,omitted:n,pruned:[],forbidden_fields:[]}}function D(e,t,n,r=null){let s=new Headers({"X-Dmp-Preview":"true"});return r!==null&&s.set("X-Dmp-Record-Version",r),{status:e,data:t,meta:n,headers:s,etag:null,recordVersion:r,location:null,replayed:!1,rateLimit:{limit:null,remaining:null,reset:null}}}function w(e,t,n,r,s,i={}){return new R(e,new Headers({"Content-Type":"application/problem+json",...i}),{type:`https://docs.cube.nl/dmp-api/problems/${t}`,title:t.replace(/_/g," "),status:e,code:t,...s!==void 0?{reason:s}:{},detail:n,...r!==void 0?{errors:r}:{}})}function yn(e,t){let n=e?.types[0];if((n==="integer"||n==="number")&&t!==""&&Number.isFinite(Number(t)))return Number(t);if(n==="boolean"){if(t==="1"||t==="true")return!0;if(t==="0"||t==="false")return!1}return t}function J(e,t){return e==null?!1:typeof e=="boolean"&&typeof t!="boolean"?(e?"1":"0")===String(t)||String(e)===String(t):typeof e=="number"&&typeof t=="string"?String(e)===t:e===t||typeof e!="object"&&String(e)===String(t)}function _(e,t){if(e==null||t==null)return e==null?t==null?0:-1:1;if(typeof e=="number"&&typeof t=="number")return e-t;if(typeof e=="boolean"||typeof t=="boolean")return+(e===!0||e===1||e==="1")-+(t===!0||t===1||t==="1");let n=String(e),r=String(t);return n<r?-1:n>r?1:0}function S(e){return e==null?"":String(e)}function x(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function B(e){return e===void 0?e:JSON.parse(JSON.stringify(e))}var Re="1.0.0-rc.4";var wn="./cube-app.config.json",bn="./cube-mock.json",kn="./cube-app.json",Rn={id:"preview",name:"Voorbeeldgebruiker",scopes:[]},Te=null;async function Tn(e={}){let t=e.fetch??globalThis.fetch.bind(globalThis),n=e.pageUrl??xn(),r=await Y(t,F(wn,n));return r===null?_n(e,t,n):Sn($n(r),e,t,n)}async function _n(e,t,n){let r=e.mock??Te??await Y(t,F(bn,n))??{},s=e.manifest===!1?null:e.manifest??await Y(t,F(kn,n)),i=!0,o=new j(r,s,()=>{if(!i)throw new R(401,new Headers({"Content-Type":"application/problem+json"}),{type:"https://docs.cube.nl/dmp-api/problems/token_invalid",title:"token invalid",status:401,code:"token_invalid",detail:"Unauthenticated."})});return{mode:"preview",signInError:null,user:()=>i?{...Rn}:null,async signIn(){i=!0},async signOut(){i=!1},resource:a=>o.resource(a)}}async function Sn(e,t,n,r){let s=e.dmpUrl.replace(/\/+$/,""),i=null,o=Dn(t.tokenStorage==="memory"?M():On(`cube-app.tokens.${e.clientId}`),m=>{i=m}),a=re({issuer:e.issuer??s,clientId:e.clientId,redirectUri:F(e.redirectUri,r),scopes:e.scopes,fetch:n,tokenStore:o,...e.metadata!==void 0?{metadata:e.metadata}:{}}),p=ae({baseUrl:e.apiUrl??`${s}/apps/api/v1`,auth:a,fetch:n}),d=null;i=await o.get();try{await a.handleCallback()}catch(m){d=m instanceof Error?m:new Error(String(m))}return i===null&&d===null&&t.autoSignIn===!0&&await a.signIn(),{mode:"live",get signInError(){return d},user:()=>i===null?null:An(i),async signIn(){d=null,await a.signIn()},async signOut(){await a.signOut()},resource:m=>p.resources(m)}}function An(e){let t=vn(e.accessToken),n=t?.sub,r=Array.isArray(t?.scopes)?t.scopes.map(String):null;return{id:typeof n=="string"||typeof n=="number"?String(n):null,name:null,scopes:r??(e.scope??"").split(" ").filter(s=>s!=="")}}function vn(e){let t=e.split(".")[1];if(t===void 0)return null;try{let n=t.replace(/-/g,"+").replace(/_/g,"/"),r=atob(n.padEnd(n.length+(4-n.length%4)%4,"=")),s=new TextDecoder().decode(Uint8Array.from(r,o=>o.charCodeAt(0))),i=JSON.parse(s);return i!==null&&typeof i=="object"?i:null}catch{return null}}function $n(e){let t=["dmpUrl","clientId","redirectUri"].filter(r=>typeof e[r]!="string"||e[r]===""),n=e.scopes;if(typeof n=="string"||Array.isArray(n)&&n.every(r=>typeof r=="string")||t.push("scopes"),t.length>0)throw new TypeError(`cube-app.config.json is incomplete: ${t.join(", ")} missing or not a string.`);return e}async function Y(e,t){let n;try{n=await e(t,{headers:{Accept:"application/json"},...typeof document<"u"?{cache:"no-store"}:{}})}catch{return null}if(n.status===404||n.status===410)return null;if(!n.ok)throw new Error(`${t} answered ${n.status}.`);try{let r=JSON.parse(await n.text());return r!==null&&typeof r=="object"&&!Array.isArray(r)?r:null}catch{return null}}function Dn(e,t){return{async get(){let n=await e.get();return t(n),n},async set(n){await e.set(n),t(n)}}}function On(e){let t=(()=>{try{return globalThis.sessionStorage??null}catch{return null}})();return t===null?M():{get(){try{let n=t.getItem(e);return n===null?null:JSON.parse(n)}catch{return null}},set(n){try{n===null?t.removeItem(e):t.setItem(e,JSON.stringify(n))}catch{}}}}function xn(){let e=globalThis;return e.document?.baseURI??e.location?.href??"http://localhost/"}function F(e,t){return new URL(e,t).href}function Pn(e){Te=e}var _e=Object.freeze({connect:Tn,mock:Pn,version:Re,sdk:"1"});globalThis.CubeApp=_e;})();
|
package/dist/preview.d.ts
CHANGED
|
@@ -25,6 +25,15 @@ export interface MockSchema {
|
|
|
25
25
|
readonly?: readonly string[];
|
|
26
26
|
/** What a row carries when the query names no `fields`, as on the REST face. Absent: every field. */
|
|
27
27
|
summary_fields?: readonly string[];
|
|
28
|
+
/** The fields `group` may name. Absent: every field. */
|
|
29
|
+
groupable?: readonly string[];
|
|
30
|
+
/** Per field the functions `fn` may apply. Absent: every field, every function. Bare `count` is always allowed. */
|
|
31
|
+
aggregatable?: Record<string, readonly string[]>;
|
|
32
|
+
/** The allowed values of each choice field, and whether a write may set it (apps#93). */
|
|
33
|
+
choices?: Record<string, {
|
|
34
|
+
values: readonly unknown[];
|
|
35
|
+
writable: boolean;
|
|
36
|
+
}>;
|
|
28
37
|
max_results?: number;
|
|
29
38
|
fields?: Record<string, MockField> | ReadonlyArray<MockField & {
|
|
30
39
|
name: string;
|
package/dist/preview.js
CHANGED
|
@@ -3,7 +3,11 @@ import { buildQuery } from './query.js';
|
|
|
3
3
|
const RESOURCE_NAME = /^[a-z][a-z0-9_]*$/;
|
|
4
4
|
const OPERATORS = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'like', 'ilike', 'is', 'between'];
|
|
5
5
|
const SYMBOLS = [['>=', 'gte'], ['<=', 'lte'], ['<>', 'neq'], ['>', 'gt'], ['<', 'lt'], ['=', 'eq']];
|
|
6
|
+
const FUNCTIONS = ['count', 'sum', 'avg', 'min', 'max'];
|
|
7
|
+
const UNITS = ['day', 'week', 'month', 'quarter', 'year'];
|
|
6
8
|
const MAX_FILTERS = 20;
|
|
9
|
+
const MAX_DIMENSIONS = 5;
|
|
10
|
+
const MAX_GROUPS = 1000;
|
|
7
11
|
const MAX_IN_VALUES = 200;
|
|
8
12
|
const DEFAULT_PAGE_SIZE = 50;
|
|
9
13
|
const MAX_PAGE_SIZE = 200;
|
|
@@ -64,7 +68,7 @@ export class PreviewStore {
|
|
|
64
68
|
}
|
|
65
69
|
},
|
|
66
70
|
get: (id, query = {}) => run('get', (table) => getRow(table, id, query)),
|
|
67
|
-
aggregate: (query) => run('
|
|
71
|
+
aggregate: (query) => run('aggregate', (table) => aggregateRows(table, query)),
|
|
68
72
|
create: (body, options = {}) => run('create', (table) => createRow(table, body, options)),
|
|
69
73
|
update: (id, body, options = {}) => run('update', (table) => updateRow(table, id, body, options)),
|
|
70
74
|
delete: (id, options = {}) => run('delete', (table) => deleteRow(table, id, options)),
|
|
@@ -120,16 +124,37 @@ function table(name, resource) {
|
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
const names = [...fields.keys()];
|
|
127
|
+
// A malformed key or entry is ignored as if absent, so one bad line in a mock never breaks the preview.
|
|
128
|
+
const choiceEntries = (isObject(schema.choices) ? Object.entries(schema.choices) : []).filter((entry) => isObject(entry[1]) && Array.isArray(entry[1].values));
|
|
123
129
|
const readonly = new Set([
|
|
124
130
|
'id',
|
|
125
131
|
...(schema.readonly ?? []),
|
|
126
132
|
...declared.filter(([, definition]) => definition.readonly === true).map(([fieldName]) => fieldName),
|
|
133
|
+
...choiceEntries
|
|
134
|
+
.filter(([, choice]) => choice.writable === false)
|
|
135
|
+
.map(([fieldName]) => fieldName),
|
|
127
136
|
]);
|
|
128
|
-
const
|
|
137
|
+
const choices = new Map();
|
|
138
|
+
for (const [fieldName, definition] of declared) {
|
|
139
|
+
if (Array.isArray(definition.enum)) {
|
|
140
|
+
choices.set(fieldName, definition.enum);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const [fieldName, choice] of choiceEntries) {
|
|
144
|
+
choices.set(fieldName, choice.values);
|
|
145
|
+
}
|
|
146
|
+
const known = (list) => Array.isArray(list) ? list.filter((item) => typeof item === 'string' && fields.has(item)) : null;
|
|
129
147
|
return {
|
|
130
148
|
name,
|
|
131
149
|
fields,
|
|
132
150
|
filterable: known(schema.filters) ?? names,
|
|
151
|
+
groupable: known(schema.groupable) ?? names,
|
|
152
|
+
aggregatable: new Map(isObject(schema.aggregatable)
|
|
153
|
+
? Object.entries(schema.aggregatable)
|
|
154
|
+
.filter(([fieldName, allowed]) => fields.has(fieldName) && Array.isArray(allowed))
|
|
155
|
+
.map(([fieldName, allowed]) => [fieldName, [...allowed]])
|
|
156
|
+
: names.map((fieldName) => [fieldName, [...FUNCTIONS]])),
|
|
157
|
+
choices,
|
|
133
158
|
writable: new Set(names.filter((fieldName) => !readonly.has(fieldName))),
|
|
134
159
|
summary: (known(schema.summary_fields) ?? []).length > 0 ? known(schema.summary_fields) : names,
|
|
135
160
|
rows,
|
|
@@ -342,14 +367,25 @@ function readPage(params) {
|
|
|
342
367
|
}
|
|
343
368
|
function aggregateRows(table, query) {
|
|
344
369
|
const params = parse(table, query);
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
370
|
+
// As the wire reads a query: fn entries, then group entries, then filters.
|
|
371
|
+
const rawFn = (params.get('fn') ?? '').trim();
|
|
372
|
+
if (rawFn === '') {
|
|
373
|
+
throw problem(422, 'query_invalid', 'fn is required: name at least one aggregation function.', {
|
|
374
|
+
fn: [`Expected e.g. fn=count or fn=sum(field). ${aggregatableHint(table)}`],
|
|
375
|
+
});
|
|
350
376
|
}
|
|
377
|
+
const functions = rawFn.split(',').map((item) => item.trim());
|
|
351
378
|
const readers = functions.map((fn) => aggregator(table, fn));
|
|
379
|
+
rejectDuplicates(functions, 'fn');
|
|
380
|
+
const rawGroup = (params.get('group') ?? '').trim();
|
|
381
|
+
const groups = rawGroup === '' ? [] : rawGroup.split(',').map((item) => item.trim());
|
|
382
|
+
// Before the entries: the repair for too many dimensions is a coarser question, not a fixed name.
|
|
383
|
+
if (groups.length > MAX_DIMENSIONS) {
|
|
384
|
+
throw problem(422, 'cardinality_exceeded', `group may not exceed ${MAX_DIMENSIONS} dimensions, ${groups.length} given.`, { group: [`Use at most ${MAX_DIMENSIONS} dimensions.`] });
|
|
385
|
+
}
|
|
352
386
|
const dimensions = groups.map((group) => dimension(table, group));
|
|
387
|
+
rejectDuplicates(groups, 'group');
|
|
388
|
+
const filters = readFilters(table, params);
|
|
353
389
|
const buckets = new Map();
|
|
354
390
|
for (const row of table.rows.filter((candidate) => filters.every((filter) => matches(table, candidate, filter)))) {
|
|
355
391
|
const key = {};
|
|
@@ -364,39 +400,86 @@ function aggregateRows(table, query) {
|
|
|
364
400
|
if (groups.length === 0 && buckets.size === 0) {
|
|
365
401
|
buckets.set('[]', { key: {}, rows: [] });
|
|
366
402
|
}
|
|
367
|
-
|
|
403
|
+
if (buckets.size > MAX_GROUPS) {
|
|
404
|
+
throw problem(422, 'cardinality_exceeded', 'The grouping produces more groups than the configured maximum.', {
|
|
405
|
+
groepen: [
|
|
406
|
+
'The grouping produces more groups than the configured maximum.',
|
|
407
|
+
`Allowed values: groepeer grover, of filter eerst; maximaal ${MAX_GROUPS} groepen.`,
|
|
408
|
+
],
|
|
409
|
+
}, 'too_many_groups');
|
|
410
|
+
}
|
|
411
|
+
const ordered = [...buckets.values()].sort((a, b) => {
|
|
412
|
+
for (const group of groups) {
|
|
413
|
+
const order = compare(a.key[group], b.key[group]);
|
|
414
|
+
if (order !== 0) {
|
|
415
|
+
return order;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return 0;
|
|
419
|
+
});
|
|
420
|
+
const data = ordered.map(({ key, rows }) => {
|
|
368
421
|
const result = { ...key };
|
|
369
422
|
for (const [index, fn] of functions.entries()) {
|
|
370
423
|
result[fn] = readers[index](rows);
|
|
371
424
|
}
|
|
372
425
|
return result;
|
|
373
426
|
});
|
|
374
|
-
return response(200, data, { ...meta(table, null, []), groups:
|
|
427
|
+
return response(200, data, { ...meta(table, null, []), groups: data.length });
|
|
428
|
+
}
|
|
429
|
+
/** As live refuses it: two entries would collapse into one response key. */
|
|
430
|
+
function rejectDuplicates(entries, parameter) {
|
|
431
|
+
const duplicates = [...new Set(entries.filter((entry, index) => entries.indexOf(entry) !== index))];
|
|
432
|
+
if (duplicates.length > 0) {
|
|
433
|
+
throw problem(422, 'query_invalid', `Duplicate ${parameter} entry: ${duplicates.join(', ')}.`, {
|
|
434
|
+
[parameter]: [`List each ${parameter} entry once.`],
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function aggregatableHint(table) {
|
|
439
|
+
return table.aggregatable.size === 0
|
|
440
|
+
? 'This resource has no aggregatable fields; only bare count is available.'
|
|
441
|
+
: `Aggregatable fields: ${[...table.aggregatable.keys()].join(', ')}.`;
|
|
375
442
|
}
|
|
376
443
|
function aggregator(table, fn) {
|
|
377
|
-
|
|
444
|
+
const call = /^([a-z]+)\(([a-z0-9_]+)\)$/.exec(fn);
|
|
445
|
+
if (call === null && !/^[a-z]+$/.test(fn)) {
|
|
446
|
+
throw problem(422, 'query_invalid', `Unparseable fn entry: ${fn}.`, {
|
|
447
|
+
fn: ['Expected `count` or `function(field)`, e.g. sum(field).'],
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
const name = call === null ? fn : call[1];
|
|
451
|
+
const target = call === null ? null : call[2];
|
|
452
|
+
if (!FUNCTIONS.includes(name)) {
|
|
453
|
+
throw problem(422, 'query_invalid', `Unknown aggregation function: ${name}.`, {
|
|
454
|
+
fn: [`Allowed values: ${FUNCTIONS.join(', ')}.`],
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
if (target === null) {
|
|
458
|
+
if (name !== 'count') {
|
|
459
|
+
throw problem(422, 'query_invalid', `${name} must be applied to a field, e.g. ${name}(field).`, {
|
|
460
|
+
fn: [aggregatableHint(table)],
|
|
461
|
+
});
|
|
462
|
+
}
|
|
378
463
|
return (rows) => rows.length;
|
|
379
464
|
}
|
|
380
|
-
const
|
|
381
|
-
if (
|
|
382
|
-
throw problem(422, 'query_invalid', `Unknown
|
|
383
|
-
fn: ['Allowed: count, count(field), sum(field), avg(field), min(field), max(field).'],
|
|
384
|
-
}, 'unknown_function');
|
|
465
|
+
const allowed = table.aggregatable.get(target);
|
|
466
|
+
if (allowed === undefined) {
|
|
467
|
+
throw problem(422, 'query_invalid', `Unknown aggregatable field: ${target}.`, { fn: [aggregatableHint(table)] });
|
|
385
468
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}, 'not_aggregatable');
|
|
469
|
+
if (!allowed.includes(name)) {
|
|
470
|
+
throw problem(422, 'query_invalid', `${name} is not allowed on ${target}.`, {
|
|
471
|
+
fn: [`Allowed functions for ${target}: ${allowed.join(', ')}.`],
|
|
472
|
+
});
|
|
391
473
|
}
|
|
392
474
|
return (rows) => {
|
|
393
475
|
const values = rows.map((row) => row[target]).filter((value) => value != null);
|
|
394
476
|
const numbers = values.map(Number).filter((value) => Number.isFinite(value));
|
|
395
477
|
switch (name) {
|
|
396
478
|
case 'count':
|
|
397
|
-
|
|
479
|
+
// Live emits count(*) whatever the field, so count(field) counts the group's rows, nulls included.
|
|
480
|
+
return rows.length;
|
|
398
481
|
case 'sum':
|
|
399
|
-
return numbers.reduce((sum, value) => sum + value, 0);
|
|
482
|
+
return numbers.length === 0 ? null : numbers.reduce((sum, value) => sum + value, 0);
|
|
400
483
|
case 'avg':
|
|
401
484
|
return numbers.length === 0 ? null : numbers.reduce((sum, value) => sum + value, 0) / numbers.length;
|
|
402
485
|
case 'min':
|
|
@@ -407,23 +490,58 @@ function aggregator(table, fn) {
|
|
|
407
490
|
};
|
|
408
491
|
}
|
|
409
492
|
function dimension(table, group) {
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
493
|
+
const parts = group.split('.');
|
|
494
|
+
if (parts.length > 2) {
|
|
495
|
+
throw problem(422, 'query_invalid', `Unparseable group entry: ${group}.`, {
|
|
496
|
+
group: ['Expected `field` or `field.unit`, e.g. published_at.month.'],
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
const [name, truncation] = parts;
|
|
500
|
+
if (!table.groupable.includes(name)) {
|
|
412
501
|
throw problem(422, 'query_invalid', `Unknown group: ${name}.`, {
|
|
413
|
-
group: [`Allowed values: ${
|
|
502
|
+
group: [`Allowed values: ${table.groupable.join(', ')}.`],
|
|
414
503
|
}, 'not_groupable');
|
|
415
504
|
}
|
|
416
|
-
const lengths = { year: 4, month: 7, day: 10 };
|
|
417
505
|
if (truncation === undefined) {
|
|
418
506
|
return (row) => row[name] ?? null;
|
|
419
507
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
508
|
+
if (!UNITS.includes(truncation)) {
|
|
509
|
+
throw problem(422, 'query_invalid', `Unknown truncation unit: ${truncation}.`, {
|
|
510
|
+
group: [`Allowed values: ${UNITS.join(', ')}.`],
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return (row) => bucket(row[name], truncation);
|
|
514
|
+
}
|
|
515
|
+
/** The date as written (first 10 characters), never shifted through a time zone; null when it is not a date. */
|
|
516
|
+
function bucket(value, unit) {
|
|
517
|
+
const match = typeof value === 'string' ? /^(\d{4})-(\d{2})-(\d{2})/.exec(value) : null;
|
|
518
|
+
if (match === null) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
522
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
523
|
+
if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
switch (unit) {
|
|
527
|
+
case 'day':
|
|
528
|
+
return match[0];
|
|
529
|
+
case 'week': {
|
|
530
|
+
// ISO week: the Thursday of the Monday-based week decides the ISO year.
|
|
531
|
+
const thursday = new Date(date.getTime() + (3 - ((date.getUTCDay() + 6) % 7)) * 86_400_000);
|
|
532
|
+
const isoYear = thursday.getUTCFullYear();
|
|
533
|
+
const week = 1 + Math.floor((thursday.getTime() - Date.UTC(isoYear, 0, 1)) / (7 * 86_400_000));
|
|
534
|
+
return `${isoYear}-W${String(week).padStart(2, '0')}`;
|
|
535
|
+
}
|
|
536
|
+
case 'month':
|
|
537
|
+
return match[0].slice(0, 7);
|
|
538
|
+
case 'quarter':
|
|
539
|
+
return `${match[1]}-Q${Math.ceil(month / 3)}`;
|
|
540
|
+
case 'year':
|
|
541
|
+
return match[1];
|
|
542
|
+
default:
|
|
543
|
+
return null;
|
|
425
544
|
}
|
|
426
|
-
return (row) => (typeof row[name] === 'string' ? row[name].slice(0, length) : null);
|
|
427
545
|
}
|
|
428
546
|
// ---------------------------------------------------------------------------------------------
|
|
429
547
|
// Writing
|
|
@@ -491,7 +609,8 @@ function validate(table, body) {
|
|
|
491
609
|
}
|
|
492
610
|
const errors = {};
|
|
493
611
|
for (const [name, value] of Object.entries(body)) {
|
|
494
|
-
const
|
|
612
|
+
const definition = table.fields.get(name);
|
|
613
|
+
const problemText = typeError(definition, value) ?? choiceError(table, definition, name, value);
|
|
495
614
|
if (problemText !== null) {
|
|
496
615
|
errors[name] = [problemText];
|
|
497
616
|
}
|
|
@@ -501,6 +620,14 @@ function validate(table, body) {
|
|
|
501
620
|
}
|
|
502
621
|
return clone(body);
|
|
503
622
|
}
|
|
623
|
+
/** Laravel's `in` rule: a value outside the field's allowed values. Runs only after the type check passed. */
|
|
624
|
+
function choiceError(table, definition, name, value) {
|
|
625
|
+
const allowed = table.choices.get(name);
|
|
626
|
+
if (allowed === undefined || (value === null && definition?.nullable === true) || allowed.includes(value)) {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
return `The selected ${name.replace(/_/g, ' ')} is invalid.`;
|
|
630
|
+
}
|
|
504
631
|
/** Laravel's rules and wording for the one declared type, as the REST face's TypeRules applies them. */
|
|
505
632
|
function typeError(definition, value) {
|
|
506
633
|
if (definition === undefined || definition.types.length !== 1) {
|
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.0.0-rc.
|
|
2
|
+
export declare const VERSION = "1.0.0-rc.4";
|
|
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.0.0-rc.
|
|
2
|
+
export const VERSION = '1.0.0-rc.4';
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cube-nl/dmp-sdk",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
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",
|