@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 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 nog in **geen
13
- enkele registry**; installeer het voorlopig uit git of uit het `npm pack`-artefact van de CI.
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
- Not published yet. Until it is:
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 git+ssh://git@gitlab.cube.nl/cube/packages/laravel/filament/dmp/sdk-js.git#5.0.x
41
- # or the tarball the CI's pack job keeps for a week:
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
- A git install runs `prepare`, which builds `dist/` with `tsc`. The package is an ES module with
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 `.year`, `.month` and `.day`.
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.2.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 | 28.7 KiB minified, 10.3 KiB gzipped (0.2.0) |
619
- | Version | The banner's first line, `/*! @cube/dmp-sdk 0.2.0 cube-sdk.js … */`, and `CubeApp.version` |
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 from a known commit or tag into the host, rather than fetching it
622
- at runtime: `npm pack git+ssh://git@gitlab.cube.nl/cube/packages/laravel/filament/dmp/sdk-js.git#<ref>`
623
- builds the tarball, `package/dist/cube-sdk.js` is the file. Nothing from a CDN, nothing at request
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.3'`. */
82
- version: "1.0.0-rc.3";
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.3'`. */
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.3 cube-sdk.js (window.CubeApp) | MIT */
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('list', (table) => aggregateRows(table, query)),
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 known = (list) => list === undefined ? null : list.filter((item) => fields.has(item));
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
- const filters = readFilters(table, params);
346
- const functions = (params.get('fn') ?? '').split(',').filter((item) => item !== '');
347
- const groups = (params.get('group') ?? '').split(',').filter((item) => item !== '');
348
- if (functions.length === 0) {
349
- throw problem(422, 'query_invalid', 'fn is required.', { fn: ['Name at least one function, e.g. fn=count.'] });
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
- const data = [...buckets.values()].map(({ key, rows }) => {
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: groups.length === 0 ? null : data.length });
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
- if (fn === 'count') {
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 match = /^(count|sum|avg|min|max)\(([^)]+)\)$/.exec(fn);
381
- if (match === null) {
382
- throw problem(422, 'query_invalid', `Unknown function: ${fn}.`, {
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
- const [, name, target] = match;
387
- if (!table.fields.has(target)) {
388
- throw problem(422, 'query_invalid', `Unknown fn: ${target}.`, {
389
- fn: [`Allowed values: ${[...table.fields.keys()].join(', ')}.`],
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
- return values.length;
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 [name, truncation] = group.split('.');
411
- if (!table.fields.has(name)) {
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: ${[...table.fields.keys()].join(', ')}.`],
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
- const length = lengths[truncation];
421
- if (length === undefined) {
422
- throw problem(422, 'query_invalid', `Unknown truncation: ${truncation}.`, {
423
- group: ['Allowed truncations in the preview: year, month, day.'],
424
- }, 'unknown_truncation');
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 problemText = typeError(table.fields.get(name), value);
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.3";
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.3';
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",
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",