@cube-nl/dmp-sdk 1.0.0-rc.4 → 1.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +298 -6
- package/dist/app.d.ts +36 -2
- package/dist/app.js +192 -10
- package/dist/browser.js +6 -1
- package/dist/client.d.ts +2 -0
- package/dist/cube-sdk.js +179 -2
- package/dist/cube-ui.manifest.json +971 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/preview.d.ts +9 -0
- package/dist/preview.js +39 -1
- package/dist/size.json +6 -0
- package/dist/ui/aggregate-block.d.ts +73 -0
- package/dist/ui/aggregate-block.js +138 -0
- package/dist/ui/attributes.d.ts +5 -0
- package/dist/ui/attributes.js +72 -0
- package/dist/ui/block.d.ts +63 -0
- package/dist/ui/block.js +229 -0
- package/dist/ui/blocks/app-shell.d.ts +4 -0
- package/dist/ui/blocks/app-shell.js +167 -0
- package/dist/ui/blocks/board.d.ts +8 -0
- package/dist/ui/blocks/board.js +292 -0
- package/dist/ui/blocks/calc.d.ts +9 -0
- package/dist/ui/blocks/calc.js +240 -0
- package/dist/ui/blocks/chart.d.ts +7 -0
- package/dist/ui/blocks/chart.js +166 -0
- package/dist/ui/blocks/detail.d.ts +8 -0
- package/dist/ui/blocks/detail.js +270 -0
- package/dist/ui/blocks/field.d.ts +8 -0
- package/dist/ui/blocks/field.js +16 -0
- package/dist/ui/blocks/form.d.ts +8 -0
- package/dist/ui/blocks/form.js +153 -0
- package/dist/ui/blocks/gap-list.d.ts +4 -0
- package/dist/ui/blocks/gap-list.js +36 -0
- package/dist/ui/blocks/kpi.d.ts +4 -0
- package/dist/ui/blocks/kpi.js +74 -0
- package/dist/ui/blocks/period-picker.d.ts +4 -0
- package/dist/ui/blocks/period-picker.js +155 -0
- package/dist/ui/blocks/state.d.ts +4 -0
- package/dist/ui/blocks/state.js +135 -0
- package/dist/ui/blocks/table.d.ts +8 -0
- package/dist/ui/blocks/table.js +607 -0
- package/dist/ui/blocks/top.d.ts +4 -0
- package/dist/ui/blocks/top.js +67 -0
- package/dist/ui/chart-svg.d.ts +28 -0
- package/dist/ui/chart-svg.js +111 -0
- package/dist/ui/csv.d.ts +9 -0
- package/dist/ui/csv.js +55 -0
- package/dist/ui/define.d.ts +24 -0
- package/dist/ui/define.js +107 -0
- package/dist/ui/dom.d.ts +12 -0
- package/dist/ui/dom.js +57 -0
- package/dist/ui/fields.d.ts +39 -0
- package/dist/ui/fields.js +73 -0
- package/dist/ui/format.d.ts +44 -0
- package/dist/ui/format.js +214 -0
- package/dist/ui/inputs.d.ts +35 -0
- package/dist/ui/inputs.js +161 -0
- package/dist/ui/manifest.d.ts +40 -0
- package/dist/ui/manifest.js +878 -0
- package/dist/ui/period.d.ts +18 -0
- package/dist/ui/period.js +52 -0
- package/dist/ui/query.d.ts +26 -0
- package/dist/ui/query.js +98 -0
- package/dist/ui/schema.d.ts +23 -0
- package/dist/ui/schema.js +21 -0
- package/dist/ui/styles.d.ts +8 -0
- package/dist/ui/styles.js +51 -0
- package/dist/ui/texts.d.ts +134 -0
- package/dist/ui/texts.js +122 -0
- package/dist/ui/theme.d.ts +58 -0
- package/dist/ui/theme.js +311 -0
- package/dist/ui/write.d.ts +20 -0
- package/dist/ui/write.js +54 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wireframe.d.ts +37 -0
- package/dist/wireframe.js +315 -0
- package/fonts/OFL-albert-sans.txt +93 -0
- package/fonts/OFL-ibm-plex-sans.txt +93 -0
- package/fonts/OFL-inter.txt +93 -0
- package/fonts/OFL-nunito-sans.txt +93 -0
- package/fonts/OFL-source-sans-3.txt +93 -0
- package/fonts/albert-sans.woff2 +0 -0
- package/fonts/ibm-plex-sans.woff2 +0 -0
- package/fonts/inter.woff2 +0 -0
- package/fonts/nunito-sans.woff2 +0 -0
- package/fonts/source-sans-3.woff2 +0 -0
- package/package.json +8 -3
package/dist/app.js
CHANGED
|
@@ -2,34 +2,165 @@ import { createOAuth, memoryTokenStore, } from './auth.js';
|
|
|
2
2
|
import { createClient } from './client.js';
|
|
3
3
|
import { DmpError } from './errors.js';
|
|
4
4
|
import { PreviewStore } from './preview.js';
|
|
5
|
+
import { schemaFromCapability } from './ui/schema.js';
|
|
6
|
+
import { PREVIEW_USER_NAME } from './ui/texts.js';
|
|
7
|
+
import { resolveTheme, setTheme } from './ui/theme.js';
|
|
5
8
|
import { MANIFEST_SDK, VERSION } from './version.js';
|
|
9
|
+
import { mixedResource, readGaps, simulatedWrites, withGaps } from './wireframe.js';
|
|
6
10
|
const CONFIG_FILE = './cube-app.config.json';
|
|
7
11
|
const MOCK_FILE = './cube-mock.json';
|
|
8
12
|
const MANIFEST_FILE = './cube-app.json';
|
|
9
|
-
const
|
|
13
|
+
const THEME_FILE = './cube-theme.json';
|
|
14
|
+
const PREVIEW_USER = { id: 'preview', name: PREVIEW_USER_NAME, scopes: [] };
|
|
10
15
|
let registeredMock = null;
|
|
16
|
+
let registeredTheme = null;
|
|
17
|
+
let pageSession = null;
|
|
18
|
+
/** The session `ready()` connected with the defaults, while it is the page session. */
|
|
19
|
+
let fallbackSession = null;
|
|
20
|
+
/** A `ready()` waiting for the page to load before it connects with the defaults. */
|
|
21
|
+
let waitingSession = null;
|
|
22
|
+
/** Counts start-ups: only the newest may apply its theme, so a slower, replaced one cannot overwrite it. */
|
|
23
|
+
let starts = 0;
|
|
11
24
|
/**
|
|
12
25
|
* Starts the app. Reads `./cube-app.config.json` next to the page: absent, the app runs in the
|
|
13
26
|
* preview on mock data and makes no request to any DMP; present, it signs the end user in with
|
|
14
27
|
* OAuth (authorization code + PKCE) and talks to the DMP's REST face. The app's code is the same.
|
|
15
28
|
*/
|
|
16
|
-
|
|
29
|
+
function connect(options = {}) {
|
|
30
|
+
const replacesFallback = pageSession !== null && pageSession === fallbackSession;
|
|
31
|
+
const generation = ++starts;
|
|
32
|
+
const session = start(options, () => generation === starts);
|
|
33
|
+
pageSession = session;
|
|
34
|
+
fallbackSession = null;
|
|
35
|
+
// A failed start is not the page session: a later ready() tries again.
|
|
36
|
+
session.then(() => {
|
|
37
|
+
// Blocks took the default session already: tell them to take the app's own.
|
|
38
|
+
if (replacesFallback && pageSession === session)
|
|
39
|
+
announceSessionChange();
|
|
40
|
+
}, () => {
|
|
41
|
+
if (pageSession === session) {
|
|
42
|
+
pageSession = null;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return session;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The session `connect()` made for this page, or one with the defaults when the app never connected. The default one
|
|
49
|
+
* starts only once the page has loaded (and one task later), so an app that awaits something before it calls
|
|
50
|
+
* `connect()` still gets there first and the page signs in once.
|
|
51
|
+
*/
|
|
52
|
+
function ready() {
|
|
53
|
+
if (pageSession !== null) {
|
|
54
|
+
return pageSession;
|
|
55
|
+
}
|
|
56
|
+
waitingSession ??= pageLoaded().then(() => {
|
|
57
|
+
waitingSession = null;
|
|
58
|
+
if (pageSession !== null) {
|
|
59
|
+
return pageSession;
|
|
60
|
+
}
|
|
61
|
+
const session = connect();
|
|
62
|
+
fallbackSession = session;
|
|
63
|
+
return session;
|
|
64
|
+
});
|
|
65
|
+
return waitingSession;
|
|
66
|
+
}
|
|
67
|
+
/** Resolves one task after the window's `load` (right away outside a page). */
|
|
68
|
+
function pageLoaded() {
|
|
69
|
+
const page = globalThis;
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const later = () => {
|
|
72
|
+
setTimeout(resolve, 0);
|
|
73
|
+
};
|
|
74
|
+
const loaded = page.document === undefined || page.document.readyState === 'complete';
|
|
75
|
+
if (loaded || page.addEventListener === undefined) {
|
|
76
|
+
later();
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
page.addEventListener('load', later, { once: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function announceSessionChange() {
|
|
84
|
+
const doc = globalThis.document;
|
|
85
|
+
if (doc === undefined) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const View = doc.defaultView?.CustomEvent ?? CustomEvent;
|
|
89
|
+
doc.dispatchEvent(new View('cube-session-change'));
|
|
90
|
+
}
|
|
91
|
+
async function start(options, isCurrent) {
|
|
17
92
|
const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
18
93
|
const pageUrl = options.pageUrl ?? currentPage();
|
|
19
|
-
const config = await
|
|
94
|
+
const [config, theme] = await Promise.all([
|
|
95
|
+
readJson(doFetch, resolve(CONFIG_FILE, pageUrl)),
|
|
96
|
+
loadTheme(options, doFetch, pageUrl),
|
|
97
|
+
]);
|
|
98
|
+
if (isCurrent()) {
|
|
99
|
+
setTheme(theme);
|
|
100
|
+
}
|
|
20
101
|
if (config === null) {
|
|
21
102
|
return preview(options, doFetch, pageUrl);
|
|
22
103
|
}
|
|
23
104
|
return live(asConfig(config), options, doFetch, pageUrl);
|
|
24
105
|
}
|
|
25
|
-
|
|
26
|
-
|
|
106
|
+
/** Never rejects: a broken theme file must not stop the app, it only costs the house style. */
|
|
107
|
+
async function loadTheme(options, doFetch, pageUrl) {
|
|
108
|
+
try {
|
|
109
|
+
if (options.theme === false) {
|
|
110
|
+
return resolveTheme(null, pageUrl);
|
|
111
|
+
}
|
|
112
|
+
const given = options.theme ?? registeredTheme;
|
|
113
|
+
if (given !== null) {
|
|
114
|
+
return resolveTheme(given, pageUrl);
|
|
115
|
+
}
|
|
116
|
+
const file = await readJson(doFetch, resolve(THEME_FILE, pageUrl));
|
|
117
|
+
if (file !== null && (file.theme !== 1 || typeof file.source !== 'object' || file.source === null)) {
|
|
118
|
+
throw new TypeError('cube-theme.json is not a theme file.');
|
|
119
|
+
}
|
|
120
|
+
return resolveTheme(file, pageUrl);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.warn(`cube-theme.json could not be used, the neutral theme applies: ${String(error)}`);
|
|
124
|
+
return resolveTheme(null, pageUrl);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** The mock data and the manifest, from the options, `CubeApp.mock()` or the files next to the page. */
|
|
128
|
+
async function appFiles(options, doFetch, pageUrl) {
|
|
129
|
+
const mock = options.mock
|
|
130
|
+
?? registeredMock
|
|
131
|
+
?? (await readJson(doFetch, resolve(MOCK_FILE, pageUrl)))
|
|
132
|
+
?? {};
|
|
27
133
|
const manifest = options.manifest === false
|
|
28
134
|
? null
|
|
29
135
|
: options.manifest ?? (await readJson(doFetch, resolve(MANIFEST_FILE, pageUrl)));
|
|
136
|
+
return [mock, manifest];
|
|
137
|
+
}
|
|
138
|
+
/** Only a wireframe has gaps: any other app marks nothing, whatever its manifest lists. */
|
|
139
|
+
function marks(soort, manifest) {
|
|
140
|
+
const wireframe = soort === 'wireframe';
|
|
141
|
+
const gaps = wireframe ? readGaps(manifest?.ontbreekt) : [];
|
|
142
|
+
return {
|
|
143
|
+
soort: soort ?? null,
|
|
144
|
+
gaps,
|
|
145
|
+
simulatesWrites: wireframe,
|
|
146
|
+
isExample: (resource, field) => gaps.some((gap) => gap.resource === resource && (gap.veld === undefined || gap.veld === field)),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** A wireframe answers every write in the browser: one simulating client per resource, kept as long as the session. */
|
|
150
|
+
function writes(on, open) {
|
|
151
|
+
const clients = new Map();
|
|
152
|
+
const simulating = (name) => {
|
|
153
|
+
const client = clients.get(name) ?? simulatedWrites(open(name), name);
|
|
154
|
+
clients.set(name, client);
|
|
155
|
+
return client;
|
|
156
|
+
};
|
|
157
|
+
return (on ? simulating : open);
|
|
158
|
+
}
|
|
159
|
+
async function preview(options, doFetch, pageUrl) {
|
|
160
|
+
const [mock, manifest] = await appFiles(options, doFetch, pageUrl);
|
|
30
161
|
let signedIn = true;
|
|
31
162
|
// Signed out, the preview refuses as the API refuses a request without a token.
|
|
32
|
-
const
|
|
163
|
+
const guard = () => {
|
|
33
164
|
if (!signedIn) {
|
|
34
165
|
throw new DmpError(401, new Headers({ 'Content-Type': 'application/problem+json' }), {
|
|
35
166
|
type: 'https://docs.cube.nl/dmp-api/problems/token_invalid',
|
|
@@ -39,8 +170,14 @@ async function preview(options, doFetch, pageUrl) {
|
|
|
39
170
|
detail: 'Unauthenticated.',
|
|
40
171
|
});
|
|
41
172
|
}
|
|
42
|
-
}
|
|
173
|
+
};
|
|
174
|
+
const marked = marks(manifest?.soort, manifest);
|
|
175
|
+
const ceiling = new PreviewStore(mock, manifest, guard);
|
|
176
|
+
// As live: a resource the DMP does not have yet is not in `uses`, so its rows come without that ceiling.
|
|
177
|
+
const examples = new PreviewStore(mock, null, guard);
|
|
178
|
+
const store = (name) => (marked.isExample(name) ? examples : ceiling);
|
|
43
179
|
return {
|
|
180
|
+
...marked,
|
|
44
181
|
mode: 'preview',
|
|
45
182
|
signInError: null,
|
|
46
183
|
user: () => (signedIn ? { ...PREVIEW_USER } : null),
|
|
@@ -50,7 +187,15 @@ async function preview(options, doFetch, pageUrl) {
|
|
|
50
187
|
async signOut() {
|
|
51
188
|
signedIn = false;
|
|
52
189
|
},
|
|
53
|
-
resource: (name) =>
|
|
190
|
+
resource: writes(marked.simulatesWrites, (name) => {
|
|
191
|
+
const own = store(name);
|
|
192
|
+
const client = own.resource(name);
|
|
193
|
+
const known = own.describe(name)?.fields ?? [];
|
|
194
|
+
// A missing field the mock data does not hold either is invented, as live: naming it would be a 422.
|
|
195
|
+
const absent = marked.gaps.filter(({ resource, veld }) => resource === name && veld !== undefined && !known.some((field) => field.name === veld));
|
|
196
|
+
return absent.length > 0 ? mixedResource(client, absent, examples, examples.rows(name)) : client;
|
|
197
|
+
}),
|
|
198
|
+
schema: async (name) => withGaps(store(name).describe(name), marked.gaps),
|
|
54
199
|
};
|
|
55
200
|
}
|
|
56
201
|
async function live(config, options, doFetch, pageUrl) {
|
|
@@ -70,6 +215,12 @@ async function live(config, options, doFetch, pageUrl) {
|
|
|
70
215
|
});
|
|
71
216
|
const client = createClient({ baseUrl: config.apiUrl ?? `${dmpUrl}/apps/api/v1`, auth: oauth, fetch: doFetch });
|
|
72
217
|
let signInError = null;
|
|
218
|
+
let capabilities = null;
|
|
219
|
+
// Only a wireframe reads the mock data: an ordinary live app never fetches cube-mock.json, even when it ships one.
|
|
220
|
+
const [mock, manifest] = config.soort === 'wireframe' ? await appFiles(options, doFetch, pageUrl) : [{}, null];
|
|
221
|
+
const marked = marks(config.soort, manifest);
|
|
222
|
+
// The manifest's ceiling is the live client's: a missing resource is not in it, so the store checks none.
|
|
223
|
+
const examples = new PreviewStore(mock);
|
|
73
224
|
tokens = await store.get();
|
|
74
225
|
try {
|
|
75
226
|
await oauth.handleCallback();
|
|
@@ -82,6 +233,7 @@ async function live(config, options, doFetch, pageUrl) {
|
|
|
82
233
|
await oauth.signIn();
|
|
83
234
|
}
|
|
84
235
|
return {
|
|
236
|
+
...marked,
|
|
85
237
|
mode: 'live',
|
|
86
238
|
get signInError() {
|
|
87
239
|
return signInError;
|
|
@@ -94,7 +246,26 @@ async function live(config, options, doFetch, pageUrl) {
|
|
|
94
246
|
async signOut() {
|
|
95
247
|
await oauth.signOut();
|
|
96
248
|
},
|
|
97
|
-
resource: (name) =>
|
|
249
|
+
resource: writes(marked.simulatesWrites, (name) => marked.isExample(name)
|
|
250
|
+
? examples.resource(name)
|
|
251
|
+
: marked.gaps.some((gap) => gap.resource === name)
|
|
252
|
+
? mixedResource(client.resources(name), marked.gaps, examples, examples.rows(name))
|
|
253
|
+
: client.resources(name)),
|
|
254
|
+
async schema(name) {
|
|
255
|
+
if (marked.isExample(name)) {
|
|
256
|
+
return withGaps(examples.describe(name), marked.gaps);
|
|
257
|
+
}
|
|
258
|
+
capabilities ??= client.capabilities();
|
|
259
|
+
try {
|
|
260
|
+
const found = (await capabilities).resources.find((resource) => resource.name === name);
|
|
261
|
+
return found === undefined ? null : withGaps(schemaFromCapability(found), marked.gaps);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
// A failed call (signed out, offline) must not stay cached for the whole session.
|
|
265
|
+
capabilities = null;
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
},
|
|
98
269
|
};
|
|
99
270
|
}
|
|
100
271
|
/**
|
|
@@ -234,11 +405,22 @@ function resolve(path, pageUrl) {
|
|
|
234
405
|
function mock(data) {
|
|
235
406
|
registeredMock = data;
|
|
236
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Registers the house style before `connect()`, for an app that holds it in code instead of shipping
|
|
410
|
+
* `cube-theme.json`, and applies it right away. `null` forgets it again (neutral).
|
|
411
|
+
*/
|
|
412
|
+
function theme(input) {
|
|
413
|
+
registeredTheme = input;
|
|
414
|
+
setTheme(resolveTheme(input, currentPage()));
|
|
415
|
+
}
|
|
237
416
|
/** What `cube-sdk.js` puts on `window`, and what this package exports as `CubeApp`. */
|
|
238
417
|
export const CubeApp = Object.freeze({
|
|
239
418
|
connect,
|
|
419
|
+
/** The session the app connected, or one connected with the defaults. What blocks use. */
|
|
420
|
+
ready,
|
|
240
421
|
mock,
|
|
241
|
-
|
|
422
|
+
theme,
|
|
423
|
+
/** This SDK's version, e.g. `'1.1.0-rc.1'`. */
|
|
242
424
|
version: VERSION,
|
|
243
425
|
/** The manifest `"sdk"` value this build serves. */
|
|
244
426
|
sdk: MANIFEST_SDK,
|
package/dist/browser.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
import { CubeApp } from './app.js';
|
|
1
|
+
import { CubeApp as Core } from './app.js';
|
|
2
|
+
import { defineBlocks } from './ui/define.js';
|
|
2
3
|
// The entry of dist/cube-sdk.js: one classic script, no imports, `window.CubeApp` for a page without a build step.
|
|
4
|
+
const CubeApp = Object.freeze({ ...Core, defineBlocks });
|
|
3
5
|
globalThis.CubeApp = CubeApp;
|
|
6
|
+
// An engine without custom elements (an old browser, a script run outside a page) keeps the plain CubeApp.
|
|
7
|
+
if (typeof customElements !== 'undefined')
|
|
8
|
+
defineBlocks();
|
package/dist/client.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface CreateOptions {
|
|
|
37
37
|
export interface WriteOptions {
|
|
38
38
|
/** A `recordVersion` from an earlier response (never the ETag). A stale one answers 409 `conflict`. */
|
|
39
39
|
ifMatch?: string;
|
|
40
|
+
/** One key per save attempt (`newIdempotencyKey()`); send it again only to retry that same body. */
|
|
41
|
+
idempotencyKey?: string;
|
|
40
42
|
fields?: readonly string[];
|
|
41
43
|
}
|
|
42
44
|
export interface OperationOptions {
|