@foxtware/mineral 0.1.25 → 0.1.26
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/.creds.yml.sample +6 -0
- package/api/linear/linearTeamGet.js +51 -8
- package/api/linear/linearTeamsGet.js +13 -0
- package/api/tableau/docs.md +8 -0
- package/api/tableau/tableauUserAdd.js +158 -0
- package/api/tableau/tableauUsersGet.js +155 -0
- package/api/utils.js +94 -0
- package/package.json +1 -1
- package/tagalys/.gitkeep +0 -0
package/.creds.yml.sample
CHANGED
|
@@ -119,3 +119,9 @@ tagalys:
|
|
|
119
119
|
CLIENT_CODE: _________________________
|
|
120
120
|
API_KEY: _____________________________
|
|
121
121
|
STORE_ID: ____________________________
|
|
122
|
+
|
|
123
|
+
tableau:
|
|
124
|
+
PAT_NAME: ______________________________
|
|
125
|
+
PAT_SECRET: ____________________________
|
|
126
|
+
SERVER_URL: ____________________________
|
|
127
|
+
SITE_CONTENT_URL: ______________________
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
// https://linear.app/developers/graphql
|
|
2
2
|
|
|
3
|
-
const { ArgsWarden } = require('../utils');
|
|
3
|
+
const { ArgsWarden, objHasAny, oneFromManyInResponse } = require('../utils');
|
|
4
4
|
const { credsValidator } = require('../validators');
|
|
5
5
|
const { linearClient } = require('../linear/linear.utils');
|
|
6
|
+
const { linearTeamsGet } = require('./linearTeamsGet');
|
|
7
|
+
|
|
8
|
+
const teamIdentifierValidator = (teamIdentifier) => objHasAny(teamIdentifier, [
|
|
9
|
+
'teamId',
|
|
10
|
+
'teamName',
|
|
11
|
+
'teamKey',
|
|
12
|
+
]);
|
|
6
13
|
|
|
7
14
|
const argsWarden = new ArgsWarden([
|
|
8
15
|
['credsPayload', credsValidator],
|
|
9
|
-
['
|
|
16
|
+
['teamIdentifier', teamIdentifierValidator],
|
|
10
17
|
]);
|
|
11
18
|
|
|
12
19
|
const linearTeamGet = async (
|
|
13
20
|
credsPayload,
|
|
14
|
-
|
|
21
|
+
teamIdentifier,
|
|
15
22
|
{
|
|
16
23
|
inspect = false,
|
|
17
24
|
fetchClient = linearClient,
|
|
@@ -20,18 +27,54 @@ const linearTeamGet = async (
|
|
|
20
27
|
|
|
21
28
|
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
22
29
|
credsPayload,
|
|
23
|
-
|
|
30
|
+
teamIdentifier,
|
|
24
31
|
});
|
|
25
32
|
if (rejectResponse) {
|
|
26
33
|
return rejectResponse;
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
let {
|
|
37
|
+
teamId,
|
|
38
|
+
teamName,
|
|
39
|
+
teamKey,
|
|
40
|
+
} = teamIdentifier;
|
|
41
|
+
|
|
42
|
+
// TODO: Return straight from the multiple teams get, if using a non-id identifier
|
|
43
|
+
if (!teamId) {
|
|
44
|
+
|
|
45
|
+
let teamFilter =
|
|
46
|
+
teamName ? { name: { eq: teamName } }
|
|
47
|
+
: teamKey ? { key: { eq: teamKey } }
|
|
48
|
+
: null;
|
|
49
|
+
|
|
50
|
+
const teamsGetResponse = await linearTeamsGet(credsPayload, { filter: teamFilter });
|
|
51
|
+
|
|
52
|
+
const teamIdProp = teamName ? 'name' : 'key';
|
|
53
|
+
const teamIdValue = teamName ? teamName : teamKey;
|
|
54
|
+
|
|
55
|
+
const teamResponse = oneFromManyInResponse(teamsGetResponse, teamIdProp, teamIdValue);
|
|
56
|
+
if (teamResponse.ok && teamResponse.data) {
|
|
57
|
+
teamId = teamResponse.data.id;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// TODO: This should be ok: true, probably
|
|
62
|
+
if (!teamId) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
data: null,
|
|
66
|
+
meta: {
|
|
67
|
+
message: `No team found with identifier ${ JSON.stringify(teamIdentifier) }`,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
29
72
|
return fetchClient.fetch({
|
|
30
73
|
requestPayload: {
|
|
31
74
|
body: {
|
|
32
75
|
query: `
|
|
33
|
-
query TeamGet($
|
|
34
|
-
team(id: $
|
|
76
|
+
query TeamGet($teamId: String!) {
|
|
77
|
+
team(id: $teamId) {
|
|
35
78
|
id
|
|
36
79
|
name
|
|
37
80
|
key
|
|
@@ -40,7 +83,7 @@ const linearTeamGet = async (
|
|
|
40
83
|
}
|
|
41
84
|
`,
|
|
42
85
|
variables: {
|
|
43
|
-
|
|
86
|
+
teamId,
|
|
44
87
|
},
|
|
45
88
|
},
|
|
46
89
|
},
|
|
@@ -66,6 +109,6 @@ curl -X POST "http://localhost:8000/linearTeamGet" \
|
|
|
66
109
|
-H "Content-Type: application/json" \
|
|
67
110
|
-d '{
|
|
68
111
|
"credsPayload": { "credsPath": "linear" },
|
|
69
|
-
"
|
|
112
|
+
"teamIdentifier": { "teamName": "Elite Four" }
|
|
70
113
|
}'
|
|
71
114
|
*/
|
|
@@ -63,4 +63,17 @@ curl -X POST "http://localhost:8000/linearTeamsGet" \
|
|
|
63
63
|
"limit": 10
|
|
64
64
|
}
|
|
65
65
|
}'
|
|
66
|
+
|
|
67
|
+
curl -X POST "http://localhost:8000/linearTeamsGet" \
|
|
68
|
+
-H "Content-Type: application/json" \
|
|
69
|
+
-d '{
|
|
70
|
+
"credsPayload": { "credsPath": "linear" },
|
|
71
|
+
"options": {
|
|
72
|
+
"filter": {
|
|
73
|
+
"name": {
|
|
74
|
+
"eq": "WF Engineering"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}'
|
|
66
79
|
*/
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Tableau
|
|
2
|
+
|
|
3
|
+
- [Developer documentation](https://www.tableau.com/developer/learning/tableau-rest-api)
|
|
4
|
+
- [REST API](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_concepts_fundamentals.htm)
|
|
5
|
+
- [REST API reference](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm)
|
|
6
|
+
- [Authentication](https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_concepts_auth.htm)
|
|
7
|
+
|
|
8
|
+
Server/site-scoped HTTPS JSON (or XML) API at `https://{server}/api/{api-version}/sites/{site-id}/...` for managing users, groups, workbooks, data sources, and other resources, authenticated via a short-lived session token obtained by signing in with a personal access token, username/password, or JWT rather than a single static API key.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const { ArgsWarden, logDeep } = require('../utils');
|
|
2
|
+
const { resolveCreds } = require('../pipelineSteps');
|
|
3
|
+
const {
|
|
4
|
+
FetchClient,
|
|
5
|
+
appendUrlToBase,
|
|
6
|
+
fetchClientCommonSteps,
|
|
7
|
+
} = require('../utils');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_API_VERSION = '3.22';
|
|
10
|
+
|
|
11
|
+
// Tableau sessions are token-based (sign in -> token, valid ~2hrs by default
|
|
12
|
+
// server config), unlike Shopify's static API key. We cache tokens per
|
|
13
|
+
// server+site so we're not re-signing-in on every single call.
|
|
14
|
+
const tokenCache = new Map();
|
|
15
|
+
|
|
16
|
+
const tableauSignIn = async ({
|
|
17
|
+
SERVER_URL,
|
|
18
|
+
PAT_NAME,
|
|
19
|
+
PAT_SECRET,
|
|
20
|
+
SITE_CONTENT_URL = '',
|
|
21
|
+
}) => {
|
|
22
|
+
const cacheKey = `${ SERVER_URL }::${ SITE_CONTENT_URL }`;
|
|
23
|
+
const cached = tokenCache.get(cacheKey);
|
|
24
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
25
|
+
return cached;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const response = await fetch(`${ SERVER_URL }/api/${ DEFAULT_API_VERSION }/auth/signin`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': 'application/json',
|
|
32
|
+
Accept: 'application/json',
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
credentials: {
|
|
36
|
+
personalAccessTokenName: PAT_NAME,
|
|
37
|
+
personalAccessTokenSecret: PAT_SECRET,
|
|
38
|
+
site: { contentUrl: SITE_CONTENT_URL },
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
throw new Error(`Tableau sign-in failed: ${ response.status } ${ await response.text() }`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { credentials } = await response.json();
|
|
48
|
+
|
|
49
|
+
const session = {
|
|
50
|
+
token: credentials.token,
|
|
51
|
+
siteId: credentials.site.id,
|
|
52
|
+
// Refresh a bit early rather than riding right up against the server's
|
|
53
|
+
// idle/absolute timeout.
|
|
54
|
+
expiresAt: Date.now() + 1000 * 60 * 60 * 2,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
tokenCache.set(cacheKey, session);
|
|
58
|
+
return session;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const useUrlAndAuthHeaders = async (state) => {
|
|
62
|
+
const { requestPayload, context } = state;
|
|
63
|
+
const { creds } = context;
|
|
64
|
+
|
|
65
|
+
const { token, siteId } = await tableauSignIn(creds);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
requestPayload: {
|
|
69
|
+
...requestPayload,
|
|
70
|
+
url: appendUrlToBase(
|
|
71
|
+
`${ creds.SERVER_URL }/api/${ DEFAULT_API_VERSION }`,
|
|
72
|
+
requestPayload.url.replace('{siteId}', siteId),
|
|
73
|
+
),
|
|
74
|
+
headers: {
|
|
75
|
+
'X-Tableau-Auth': token,
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
Accept: 'application/json',
|
|
78
|
+
...requestPayload.headers,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const tableauClient = new FetchClient({
|
|
85
|
+
pipeline: [
|
|
86
|
+
resolveCreds,
|
|
87
|
+
useUrlAndAuthHeaders,
|
|
88
|
+
'fetch',
|
|
89
|
+
fetchClientCommonSteps.exitEarlyOnNotOk,
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const argsWarden = new ArgsWarden([
|
|
94
|
+
['username'],
|
|
95
|
+
['siteRole'],
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
// siteRole is the "whatever permissions" lever -> any valid Tableau site
|
|
99
|
+
// role: Creator, Explorer, ExplorerCanPublish, Viewer, SiteAdministratorCreator,
|
|
100
|
+
// SiteAdministratorExplorer, Unlicensed, etc.
|
|
101
|
+
const tableauUserAdd = async (
|
|
102
|
+
username,
|
|
103
|
+
siteRole = 'Viewer',
|
|
104
|
+
{
|
|
105
|
+
authSetting, // optional: 'ServerDefault' | 'SAML' | 'OpenID' | 'TableauIDWithMFA' ...
|
|
106
|
+
} = {},
|
|
107
|
+
) => {
|
|
108
|
+
const rejectResponse = await argsWarden.responseIfRejectingArgs({
|
|
109
|
+
username,
|
|
110
|
+
siteRole,
|
|
111
|
+
});
|
|
112
|
+
if (rejectResponse) {
|
|
113
|
+
return rejectResponse;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const response = await tableauClient.fetch({
|
|
117
|
+
context: {
|
|
118
|
+
credsPayload: { credsPath: ['tableau'] },
|
|
119
|
+
},
|
|
120
|
+
requestPayload: {
|
|
121
|
+
url: '/sites/{siteId}/users',
|
|
122
|
+
method: 'post',
|
|
123
|
+
body: {
|
|
124
|
+
user: {
|
|
125
|
+
name: username,
|
|
126
|
+
siteRole,
|
|
127
|
+
...(authSetting ? { authSetting } : {}),
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const { ok, data, error } = response;
|
|
134
|
+
if (!ok) {
|
|
135
|
+
logDeep({ error });
|
|
136
|
+
return { ok: false, error };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
data: data.user ?? data,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const funcApiConfig = {
|
|
146
|
+
argsWarden,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
tableauUserAdd,
|
|
151
|
+
tableauClient,
|
|
152
|
+
funcApiConfig,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/*
|
|
156
|
+
curl -X POST "http://localhost:8000/tableauUserAdd" \
|
|
157
|
+
-d '{ "username": "dora@example.com", "siteRole": "Explorer" }'
|
|
158
|
+
*/
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// GET /api/{api-version}/sites/{site-id}/users is paginated (default pageSize
|
|
2
|
+
// is 100, max 1000), so this walks all pages and returns the full list.
|
|
3
|
+
|
|
4
|
+
const { ArgsWarden, logDeep } = require('../utils');
|
|
5
|
+
const { resolveCreds } = require('../pipelineSteps');
|
|
6
|
+
const {
|
|
7
|
+
FetchClient,
|
|
8
|
+
appendUrlToBase,
|
|
9
|
+
fetchClientCommonSteps,
|
|
10
|
+
} = require('../utils');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_API_VERSION = '3.22';
|
|
13
|
+
const PAGE_SIZE = 1000; // Tableau's max page size
|
|
14
|
+
|
|
15
|
+
const tokenCache = new Map();
|
|
16
|
+
|
|
17
|
+
const tableauSignIn = async ({
|
|
18
|
+
SERVER_URL,
|
|
19
|
+
PAT_NAME,
|
|
20
|
+
PAT_SECRET,
|
|
21
|
+
SITE_CONTENT_URL = '',
|
|
22
|
+
}) => {
|
|
23
|
+
const cacheKey = `${ SERVER_URL }::${ SITE_CONTENT_URL }`;
|
|
24
|
+
const cached = tokenCache.get(cacheKey);
|
|
25
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
26
|
+
return cached;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const response = await fetch(`${ SERVER_URL }/api/${ DEFAULT_API_VERSION }/auth/signin`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
Accept: 'application/json',
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
credentials: {
|
|
37
|
+
personalAccessTokenName: PAT_NAME,
|
|
38
|
+
personalAccessTokenSecret: PAT_SECRET,
|
|
39
|
+
site: { contentUrl: SITE_CONTENT_URL },
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Error(`Tableau sign-in failed: ${ response.status } ${ await response.text() }`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { credentials } = await response.json();
|
|
49
|
+
|
|
50
|
+
const session = {
|
|
51
|
+
token: credentials.token,
|
|
52
|
+
siteId: credentials.site.id,
|
|
53
|
+
expiresAt: Date.now() + 1000 * 60 * 60 * 2,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
tokenCache.set(cacheKey, session);
|
|
57
|
+
return session;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const useUrlAndAuthHeaders = async (state) => {
|
|
61
|
+
const { requestPayload, context } = state;
|
|
62
|
+
const { creds } = context;
|
|
63
|
+
|
|
64
|
+
const { token, siteId } = await tableauSignIn(creds);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
requestPayload: {
|
|
68
|
+
...requestPayload,
|
|
69
|
+
url: appendUrlToBase(
|
|
70
|
+
`${ creds.SERVER_URL }/api/${ DEFAULT_API_VERSION }`,
|
|
71
|
+
requestPayload.url.replace('{siteId}', siteId),
|
|
72
|
+
),
|
|
73
|
+
headers: {
|
|
74
|
+
'X-Tableau-Auth': token,
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
Accept: 'application/json',
|
|
77
|
+
...requestPayload.headers,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const tableauClient = new FetchClient({
|
|
84
|
+
pipeline: [
|
|
85
|
+
resolveCreds,
|
|
86
|
+
useUrlAndAuthHeaders,
|
|
87
|
+
'fetch',
|
|
88
|
+
fetchClientCommonSteps.exitEarlyOnNotOk,
|
|
89
|
+
],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const argsWarden = new ArgsWarden([]);
|
|
93
|
+
|
|
94
|
+
// Optional filter, e.g. { filter: "siteRole:eq:Explorer" } or
|
|
95
|
+
// { filter: "name:has:jane" } per Tableau's filter-expression syntax.
|
|
96
|
+
const tableauUsersGet = async ({ filter } = {}) => {
|
|
97
|
+
const rejectResponse = await argsWarden.responseIfRejectingArgs({});
|
|
98
|
+
if (rejectResponse) {
|
|
99
|
+
return rejectResponse;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const users = [];
|
|
103
|
+
let pageNumber = 1;
|
|
104
|
+
|
|
105
|
+
while (true) {
|
|
106
|
+
const query = new URLSearchParams({
|
|
107
|
+
pageSize: String(PAGE_SIZE),
|
|
108
|
+
pageNumber: String(pageNumber),
|
|
109
|
+
...(filter ? { filter } : {}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const response = await tableauClient.fetch({
|
|
113
|
+
context: {
|
|
114
|
+
credsPayload: { credsPath: ['tableau'] },
|
|
115
|
+
},
|
|
116
|
+
requestPayload: {
|
|
117
|
+
url: `/sites/{siteId}/users?${ query.toString() }`,
|
|
118
|
+
method: 'get',
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const { ok, data, error } = response;
|
|
123
|
+
if (!ok) {
|
|
124
|
+
logDeep({ error });
|
|
125
|
+
return { ok: false, error };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const pageUsers = data?.users?.user ?? [];
|
|
129
|
+
users.push(...pageUsers);
|
|
130
|
+
|
|
131
|
+
const totalAvailable = Number(data?.pagination?.totalAvailable ?? pageUsers.length);
|
|
132
|
+
if (users.length >= totalAvailable || pageUsers.length === 0) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pageNumber += 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
data: users,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
tableauUsersGet,
|
|
147
|
+
tableauClient,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/*
|
|
151
|
+
curl -X POST "http://localhost:8000/tableauUsersGet"
|
|
152
|
+
|
|
153
|
+
curl -X POST "http://localhost:8000/tableauUsersGet"
|
|
154
|
+
-d '{ "filter": "siteRole:eq:Creator" }'
|
|
155
|
+
*/
|
package/api/utils.js
CHANGED
|
@@ -1319,6 +1319,98 @@ const objectMatchesPartial = (object, partial) => {
|
|
|
1319
1319
|
));
|
|
1320
1320
|
};
|
|
1321
1321
|
|
|
1322
|
+
const oneFromManyInResponse = (
|
|
1323
|
+
response,
|
|
1324
|
+
idProp,
|
|
1325
|
+
idValue,
|
|
1326
|
+
) => {
|
|
1327
|
+
const {
|
|
1328
|
+
ok,
|
|
1329
|
+
data,
|
|
1330
|
+
} = response;
|
|
1331
|
+
|
|
1332
|
+
if (!ok || !data) {
|
|
1333
|
+
return response;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const candidates = data.filter(item => item[idProp] === idValue);
|
|
1337
|
+
|
|
1338
|
+
if (candidates.length === 0) {
|
|
1339
|
+
return {
|
|
1340
|
+
ok: true,
|
|
1341
|
+
data: null,
|
|
1342
|
+
meta: {
|
|
1343
|
+
message: `No item found with ${ idProp } ${ idValue }`,
|
|
1344
|
+
},
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// TODO: Consider whether to return ok: false as the user's id is not sufficient
|
|
1349
|
+
if (candidates.length > 1) {
|
|
1350
|
+
return {
|
|
1351
|
+
ok: true,
|
|
1352
|
+
data: null,
|
|
1353
|
+
meta: {
|
|
1354
|
+
message: `Multiple items found with ${ idProp } ${ idValue }`,
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
return {
|
|
1360
|
+
ok: true,
|
|
1361
|
+
data: candidates[0],
|
|
1362
|
+
};
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
const surveyObject = (
|
|
1366
|
+
object,
|
|
1367
|
+
options = {},
|
|
1368
|
+
) => {
|
|
1369
|
+
|
|
1370
|
+
const {
|
|
1371
|
+
includeSamples,
|
|
1372
|
+
} = options;
|
|
1373
|
+
|
|
1374
|
+
return Object.fromEntries(
|
|
1375
|
+
Object.entries(object).map(([key, value]) => {
|
|
1376
|
+
|
|
1377
|
+
// Arrays
|
|
1378
|
+
if (Array.isArray(value)) {
|
|
1379
|
+
|
|
1380
|
+
if (includeSamples) {
|
|
1381
|
+
return [key, {
|
|
1382
|
+
samples: value.slice(0, 5),
|
|
1383
|
+
length: value.length,
|
|
1384
|
+
}];
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
return [key, value.length];
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// Sets
|
|
1391
|
+
if (value instanceof Set) {
|
|
1392
|
+
|
|
1393
|
+
if (includeSamples) {
|
|
1394
|
+
return [key, {
|
|
1395
|
+
samples: new Set(Array.from(value).slice(0, 5)),
|
|
1396
|
+
size: value.size,
|
|
1397
|
+
}];
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
return [key, value.size];
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Objects
|
|
1404
|
+
if (isObject(value)) {
|
|
1405
|
+
return [key, surveyObject(value, options)];
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Anything else
|
|
1409
|
+
return [key, value];
|
|
1410
|
+
})
|
|
1411
|
+
);
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1322
1414
|
module.exports = {
|
|
1323
1415
|
wait,
|
|
1324
1416
|
timeMs,
|
|
@@ -1358,4 +1450,6 @@ module.exports = {
|
|
|
1358
1450
|
valueProvided,
|
|
1359
1451
|
objectToArray,
|
|
1360
1452
|
objectMatchesPartial,
|
|
1453
|
+
oneFromManyInResponse,
|
|
1454
|
+
surveyObject,
|
|
1361
1455
|
};
|
package/package.json
CHANGED
package/tagalys/.gitkeep
DELETED
|
File without changes
|