@cube-nl/dmp-sdk 0.0.0-stage → 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.
@@ -0,0 +1,9 @@
1
+ import type { AggregateQuery, GetQuery, ListQuery } from './types.js';
2
+ /**
3
+ * Turn a query description into cube/api's query string, without the leading `?`.
4
+ *
5
+ * Mistakes the API would refuse anyway, and that this SDK can see coming, are thrown here as a
6
+ * TypeError before a request is made: a bare array as a filter value (`filter[x][]=a` names no
7
+ * operator and is refused — write `{ in: [...] }`), or a list under an operator that takes one value.
8
+ */
9
+ export declare function buildQuery(resource: string, query?: ListQuery & GetQuery & Partial<AggregateQuery>): string;
package/dist/query.js ADDED
@@ -0,0 +1,121 @@
1
+ const LIST_OPERATORS = new Set(['in', 'between']);
2
+ /**
3
+ * Turn a query description into cube/api's query string, without the leading `?`.
4
+ *
5
+ * Mistakes the API would refuse anyway, and that this SDK can see coming, are thrown here as a
6
+ * TypeError before a request is made: a bare array as a filter value (`filter[x][]=a` names no
7
+ * operator and is refused — write `{ in: [...] }`), or a list under an operator that takes one value.
8
+ */
9
+ export function buildQuery(resource, query = {}) {
10
+ const params = new URLSearchParams();
11
+ appendFields(params, resource, query.fields);
12
+ appendFilters(params, query.filter);
13
+ appendList(params, 'sort', query.sort);
14
+ appendList(params, 'include', query.include);
15
+ appendList(params, 'with_count', query.withCount);
16
+ appendHas(params, query.has);
17
+ appendList(params, 'fn', query.fn);
18
+ appendList(params, 'group', query.group);
19
+ if (query.page?.size !== undefined) {
20
+ params.append('page[size]', String(query.page.size));
21
+ }
22
+ if (query.page?.number !== undefined) {
23
+ params.append('page[number]', String(query.page.number));
24
+ }
25
+ for (const [key, value] of Object.entries(query.params ?? {})) {
26
+ params.append(key, value);
27
+ }
28
+ // The API reads both forms; the raw one is what the README and the error messages show. All
29
+ // four characters are legal unescaped in a query string (RFC 3986 section 3.4).
30
+ return params
31
+ .toString()
32
+ .replace(/%5B/gi, '[')
33
+ .replace(/%5D/gi, ']')
34
+ .replace(/%2C/gi, ',')
35
+ .replace(/%28/gi, '(')
36
+ .replace(/%29/gi, ')');
37
+ }
38
+ function appendFields(params, resource, fields) {
39
+ if (fields === undefined) {
40
+ return;
41
+ }
42
+ const perResource = isStringArray(fields) ? { [resource]: fields } : fields;
43
+ for (const [name, list] of Object.entries(perResource)) {
44
+ if (list.length > 0) {
45
+ params.append(`fields[${name}]`, list.join(','));
46
+ }
47
+ }
48
+ }
49
+ function appendFilters(params, filter) {
50
+ for (const [field, value] of Object.entries(filter ?? {})) {
51
+ if (Array.isArray(value)) {
52
+ throw new TypeError(`filter.${field}: a bare list names no operator and cube/api refuses it. `
53
+ + `Write { in: [...] } (or { between: [from, to] }).`);
54
+ }
55
+ if (value === null || value === undefined) {
56
+ throw new TypeError(`filter.${field}: write { is: 'null' } or { is: 'not_null' } to filter on null.`);
57
+ }
58
+ if (typeof value !== 'object') {
59
+ params.append(`filter[${field}]`, scalar(value));
60
+ continue;
61
+ }
62
+ for (const [operator, operand] of Object.entries(value)) {
63
+ if (operand === undefined) {
64
+ continue;
65
+ }
66
+ params.append(`filter[${field}][${operator}]`, operatorValue(field, operator, operand));
67
+ }
68
+ }
69
+ }
70
+ function operatorValue(field, operator, operand) {
71
+ if (operand === null) {
72
+ if (operator !== 'is') {
73
+ throw new TypeError(`filter.${field}.${operator}: null is only meaningful under 'is'.`);
74
+ }
75
+ return 'null';
76
+ }
77
+ if (Array.isArray(operand)) {
78
+ if (!LIST_OPERATORS.has(operator)) {
79
+ throw new TypeError(`filter.${field}.${operator}: takes one value; only 'in' and 'between' take a list.`);
80
+ }
81
+ return operand.map(scalar).join(',');
82
+ }
83
+ return scalar(operand);
84
+ }
85
+ function appendHas(params, has) {
86
+ for (const [relation, value] of Object.entries(has ?? {})) {
87
+ if (value === true) {
88
+ params.append(`has[${relation}]`, '1');
89
+ }
90
+ else if (typeof value === 'number') {
91
+ params.append(`has[${relation}][min]`, String(value));
92
+ }
93
+ else {
94
+ if (value.min !== undefined) {
95
+ params.append(`has[${relation}][min]`, String(value.min));
96
+ }
97
+ if (value.max !== undefined) {
98
+ params.append(`has[${relation}][max]`, String(value.max));
99
+ }
100
+ }
101
+ }
102
+ }
103
+ function appendList(params, key, value) {
104
+ if (value === undefined) {
105
+ return;
106
+ }
107
+ const joined = typeof value === 'string' ? value : value.filter((item) => item !== '').join(',');
108
+ if (joined !== '') {
109
+ params.append(key, joined);
110
+ }
111
+ }
112
+ /** Booleans as 1/0, which is what the API's own examples (`filter[active]=1`) use. */
113
+ function scalar(value) {
114
+ if (typeof value === 'boolean') {
115
+ return value ? '1' : '0';
116
+ }
117
+ return String(value);
118
+ }
119
+ function isStringArray(value) {
120
+ return Array.isArray(value);
121
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * The wire format of cube/api on 5.0.x, as types. Field names are the API's own (snake_case),
3
+ * deliberately not camelCased: a caller reading the README, `_capabilities` or a raw response in
4
+ * the network tab sees the same names here.
5
+ */
6
+ /** A record as the API returns it. Plug in your own type with the generic on `resources<T>()`. */
7
+ export type DmpRecord = Record<string, unknown>;
8
+ /** Every filter operator `filter[{field}][{operator}]` accepts (`_capabilities.operators`). */
9
+ export type FilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'like' | 'ilike' | 'is' | 'between';
10
+ export type Scalar = string | number | boolean;
11
+ /**
12
+ * One field's filter.
13
+ *
14
+ * - a scalar: equality, `filter[status]=active` (a string may carry a prefix such as `'>=4'`);
15
+ * - an object of named operators: `{ gte: 8, lte: 10 }` gives `filter[hours][gte]=8&filter[hours][lte]=10`.
16
+ * `in` and `between` take an array, `is` takes `'null'`, `'not_null'` or `null`.
17
+ */
18
+ export type FilterValue = Scalar | {
19
+ [operator in FilterOperator]?: Scalar | readonly Scalar[] | null;
20
+ };
21
+ export interface PageQuery {
22
+ /** Default 50, maximum 200 (see `_capabilities.pagination`). Above the maximum is a 422, never a clamp. */
23
+ size?: number;
24
+ /** From 1. */
25
+ number?: number;
26
+ }
27
+ /** `has[{relation}]`: `true` is "has any", a number is "at least", `{ min, max }` a range. */
28
+ export type HasValue = true | number | {
29
+ min?: number;
30
+ max?: number;
31
+ };
32
+ export interface ListQuery {
33
+ /**
34
+ * `fields[{resource}]`. An array applies to the resource being queried. cube/api reads only the
35
+ * queried resource's entry: an included relation always comes back with its own summary.
36
+ */
37
+ fields?: readonly string[] | Record<string, readonly string[]>;
38
+ filter?: Record<string, FilterValue>;
39
+ /** `'title'` or `'-title'` for descending; several are applied in order. */
40
+ sort?: string | readonly string[];
41
+ page?: PageQuery;
42
+ /** Relations, one level deep. */
43
+ include?: string | readonly string[];
44
+ /** Has-many relations to count; each row gets `{relation}_aantal`, which is also sortable. */
45
+ withCount?: string | readonly string[];
46
+ has?: Record<string, HasValue>;
47
+ /**
48
+ * Raw query parameters, appended last. For a parameter this SDK does not know yet; prefer the
49
+ * typed options above.
50
+ */
51
+ params?: Record<string, string>;
52
+ }
53
+ export interface GetQuery {
54
+ fields?: readonly string[] | Record<string, readonly string[]>;
55
+ include?: string | readonly string[];
56
+ withCount?: string | readonly string[];
57
+ params?: Record<string, string>;
58
+ }
59
+ export interface AggregateQuery {
60
+ /** `count`, `sum(hours)`, `avg(hours)`, `min(x)`, `max(x)`. The response keys are exactly these strings. */
61
+ fn: string | readonly string[];
62
+ /** Dimensions, optionally truncated: `department_id`, `started_on.month`. */
63
+ group?: string | readonly string[];
64
+ filter?: Record<string, FilterValue>;
65
+ has?: Record<string, HasValue>;
66
+ params?: Record<string, string>;
67
+ }
68
+ export interface PageMeta {
69
+ size: number;
70
+ number: number;
71
+ /** Rows in the whole scoped and filtered set, not on this page. */
72
+ total: number;
73
+ pages: number;
74
+ }
75
+ export interface Meta {
76
+ resource: string;
77
+ contract: string;
78
+ page?: PageMeta | null;
79
+ /** `_aggregate` only. */
80
+ groups?: number | null;
81
+ /** Requested relations that came back with no data because the caller may not see them. */
82
+ omitted?: string[];
83
+ /** Relations that came back but are not guaranteed complete ("3 of 8 shown"). */
84
+ pruned?: string[];
85
+ /** Fields the caller may not read, withheld from an otherwise normal 200. */
86
+ forbidden_fields?: string[];
87
+ }
88
+ export interface RateLimit {
89
+ limit: number | null;
90
+ remaining: number | null;
91
+ reset: number | null;
92
+ }
93
+ /** What every successful call returns: the body's `data`/`meta` plus the headers that matter. */
94
+ export interface DmpResponse<T> {
95
+ status: number;
96
+ data: T;
97
+ meta: Meta | null;
98
+ headers: Headers;
99
+ /** Weak representation validator. For `If-None-Match`, never for `If-Match`. */
100
+ etag: string | null;
101
+ /** Strong record validator: what `ifMatch` takes. On detail, create, update and a 409. */
102
+ recordVersion: string | null;
103
+ /** On a 201: the new record's absolute URL (`https://dmp.example.nl/apps/api/v1/projects/7`). */
104
+ location: string | null;
105
+ /** `Idempotency-Replayed: true`: this is the stored answer to an earlier attempt, nothing was written now. */
106
+ replayed: boolean;
107
+ rateLimit: RateLimit;
108
+ }
109
+ export interface ListResponse<T> extends DmpResponse<T[]> {
110
+ meta: Meta & {
111
+ page: PageMeta;
112
+ };
113
+ }
114
+ export interface OperationResponse<T> extends DmpResponse<T | null> {
115
+ /** 202: the work was queued, not done. */
116
+ accepted: boolean;
117
+ }
118
+ export interface FieldCapability {
119
+ name: string;
120
+ /** A JSON Schema primitive, or several joined with `|` for a union (`string|integer`). */
121
+ type: string;
122
+ nullable: boolean;
123
+ readonly: boolean;
124
+ description?: string | null;
125
+ }
126
+ export interface OperationCapability {
127
+ name: string;
128
+ description: string;
129
+ target: 'record' | 'collection';
130
+ /** false: `Idempotency-Key` is required, not merely honoured. */
131
+ idempotent: boolean;
132
+ input: Record<string, unknown>;
133
+ required: string[];
134
+ /** The resource it answers with; null means 204. */
135
+ output?: string | null;
136
+ }
137
+ export interface ResourceCapability {
138
+ name: string;
139
+ description: string;
140
+ actions: Array<'list' | 'get' | 'create' | 'update' | 'delete' | (string & {})>;
141
+ operations: OperationCapability[];
142
+ fields: FieldCapability[];
143
+ summary: string[];
144
+ filterable: string[];
145
+ sortable: string[];
146
+ groupable: string[];
147
+ aggregatable: Record<string, string[]>;
148
+ relations: Record<string, {
149
+ resource: string;
150
+ kind: string;
151
+ }>;
152
+ sensitivity: 'low' | 'normal' | 'high' | (string & {});
153
+ countable?: string[];
154
+ }
155
+ /** `GET /_capabilities`: narrowed to exactly what this token and this user may do. */
156
+ export interface Capabilities {
157
+ contract: string;
158
+ resources: ResourceCapability[];
159
+ aggregations: string[];
160
+ operators: string[];
161
+ operator_symbols: string[];
162
+ pagination: {
163
+ default_size: number;
164
+ max_size: number;
165
+ } & Record<string, number>;
166
+ }
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The wire format of cube/api on 5.0.x, as types. Field names are the API's own (snake_case),
3
+ * deliberately not camelCased: a caller reading the README, `_capabilities` or a raw response in
4
+ * the network tab sees the same names here.
5
+ */
6
+ export {};
@@ -0,0 +1,4 @@
1
+ /** This package's version. A test holds it equal to package.json's. */
2
+ export declare const VERSION = "1.0.0-rc.4";
3
+ /** The `"sdk"` value a `cube-app.json` manifest names for the API `CubeApp` offers here. */
4
+ export declare const MANIFEST_SDK = "1";
@@ -0,0 +1,4 @@
1
+ /** This package's version. A test holds it equal to package.json's. */
2
+ export const VERSION = '1.0.0-rc.4';
3
+ /** The `"sdk"` value a `cube-app.json` manifest names for the API `CubeApp` offers here. */
4
+ export const MANIFEST_SDK = '1';
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@cube-nl/dmp-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-rc.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
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./cube-sdk.js": "./dist/cube-sdk.js"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "tag": "next"
24
+ },
25
+ "sideEffects": false,
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json && node scripts/build-browser.mjs",
31
+ "lint": "tsc -p tsconfig.json --noEmit",
32
+ "test": "npm run build && node --test test/*.test.js",
33
+ "prepare": "npm run build",
34
+ "test:integration": "npm run build && node --test test/integration.test.js"
35
+ },
36
+ "devDependencies": {
37
+ "esbuild": "~0.28.2",
38
+ "typescript": "~5.9.3"
39
+ }
40
+ }