@foxtware/mineral 0.1.36 → 0.1.37

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.
Files changed (38) hide show
  1. package/.creds.yml.sample +9 -0
  2. package/api/marketingcloud/docs.md +6 -0
  3. package/api/marketingcloud/marketingcloud.constants.js +5 -0
  4. package/api/marketingcloud/marketingcloud.utils.js +67 -0
  5. package/api/marketingcloud/marketingcloudAttributesSearch.js +87 -0
  6. package/api/marketingcloud/marketingcloudAuthGet.js +111 -0
  7. package/api/marketingcloud/marketingcloudContactGet.js +79 -0
  8. package/api/marketingcloud/marketingcloudContactSearch.js +87 -0
  9. package/api/marketingcloud/marketingcloudSmsSubscriptionsGet.js +79 -0
  10. package/api/peoplevox/peoplevoxItemEdit.js +59 -22
  11. package/api/peoplevox/peoplevoxOrderEdit.js +49 -25
  12. package/api/shopify/shopifyInventoryItemUpdate.js +22 -16
  13. package/api/shopify/shopifyInventoryItemsUpdateBulk.js +79 -0
  14. package/api/shopify/shopifyMetafieldsSetBulk.js +87 -0
  15. package/api/shopify/shopifyProductGet.js +1 -0
  16. package/api/shopify/shopifyProductUpdate.js +136 -0
  17. package/api/shopify/shopifyProductUpdateTrigger.js +96 -0
  18. package/api/snowflake/docs.md +8 -2
  19. package/api/snowflake/snowflake.constants.js +19 -0
  20. package/api/snowflake/snowflake.utils.js +326 -0
  21. package/api/snowflake/snowflakeAuthGet.js +73 -0
  22. package/api/snowflake/snowflakeDatabasesGet.js +62 -0
  23. package/api/snowflake/snowflakeDynamicTablesGet.js +72 -0
  24. package/api/snowflake/snowflakeEventTablesGet.js +72 -0
  25. package/api/snowflake/snowflakeGet.js +153 -0
  26. package/api/snowflake/snowflakeIntegrationsGet.js +59 -0
  27. package/api/snowflake/snowflakeQuery.js +227 -0
  28. package/api/snowflake/snowflakeSchemasGet.js +69 -0
  29. package/api/snowflake/snowflakeStatementCancel.js +75 -0
  30. package/api/snowflake/snowflakeStatementExecute.js +105 -0
  31. package/api/snowflake/snowflakeStatementGet.js +80 -0
  32. package/api/snowflake/snowflakeTablesGet.js +72 -0
  33. package/api/snowflake/snowflakeUsersGet.js +60 -0
  34. package/api/snowflake/snowflakeWarehousesGet.js +43 -0
  35. package/api/starshipit/starshipitProductUpdate.js +1 -1
  36. package/api/starshipit/starshipitProductsGet.js +13 -6
  37. package/api/utils.js +62 -3
  38. package/package.json +1 -1
@@ -0,0 +1,153 @@
1
+ // https://docs.snowflake.com/en/developer-guide/snowflake-rest-api
2
+ // REST list endpoints paginate with showLimit + fromName (last item name).
3
+
4
+ const { ArgsWarden, Getter } = require('../utils');
5
+ const { credsValidator } = require('../validators');
6
+ const { snowflakeClient } = require('../snowflake/snowflake.utils');
7
+ const {
8
+ DEFAULT_SHOW_LIMIT,
9
+ MAX_SHOW_LIMIT,
10
+ } = require('../snowflake/snowflake.constants');
11
+
12
+ const argsWarden = new ArgsWarden([
13
+ ['credsPayload', credsValidator],
14
+ ['url'],
15
+ ]);
16
+
17
+ const digResults = (data) => {
18
+ if (Array.isArray(data)) {
19
+ return data;
20
+ }
21
+ if (Array.isArray(data?.data)) {
22
+ return data.data;
23
+ }
24
+ if (Array.isArray(data?.result)) {
25
+ return data.result;
26
+ }
27
+ return [];
28
+ };
29
+
30
+ const snowflakeGetPacket = async (
31
+ credsPayload,
32
+ url,
33
+ {
34
+ params,
35
+ showLimit = DEFAULT_SHOW_LIMIT,
36
+ fetchClient = snowflakeClient,
37
+ } = {},
38
+ ) => {
39
+ return fetchClient.fetch({
40
+ requestPayload: {
41
+ method: 'get',
42
+ url,
43
+ params: {
44
+ showLimit: Math.min(showLimit, MAX_SHOW_LIMIT),
45
+ ...params,
46
+ },
47
+ },
48
+ context: {
49
+ credsPayload,
50
+ },
51
+ });
52
+ };
53
+
54
+ const snowflakeGetPaginator = async (currentParams, response) => {
55
+ if (!response?.ok) {
56
+ return [true];
57
+ }
58
+
59
+ const { args, options } = currentParams;
60
+ const {
61
+ params = {},
62
+ showLimit = DEFAULT_SHOW_LIMIT,
63
+ } = options;
64
+
65
+ const items = digResults(response.data);
66
+ const limit = Number(params.showLimit ?? showLimit);
67
+
68
+ if (!items.length || items.length < limit) {
69
+ return [true];
70
+ }
71
+
72
+ const last = items[items.length - 1];
73
+ const lastName = last?.name;
74
+ if (!lastName) {
75
+ return [true];
76
+ }
77
+
78
+ return [false, {
79
+ args,
80
+ options: {
81
+ ...options,
82
+ params: {
83
+ ...params,
84
+ showLimit: limit,
85
+ fromName: lastName,
86
+ },
87
+ },
88
+ }];
89
+ };
90
+
91
+ const snowflakeGet = async (
92
+ returnGetter,
93
+
94
+ credsPayload,
95
+ url,
96
+ {
97
+ params,
98
+ showLimit = DEFAULT_SHOW_LIMIT,
99
+ fetchClient,
100
+ ...getterOptions
101
+ } = {},
102
+ ) => {
103
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
104
+ credsPayload,
105
+ url,
106
+ });
107
+ if (rejectResponse) {
108
+ return rejectResponse;
109
+ }
110
+
111
+ const getter = new Getter(
112
+ {
113
+ args: [credsPayload, url],
114
+ options: {
115
+ params,
116
+ showLimit,
117
+ fetchClient,
118
+ },
119
+ },
120
+ {
121
+ func: snowflakeGetPacket,
122
+ digester: (response) => {
123
+ if (!response?.ok) {
124
+ return [];
125
+ }
126
+ return digResults(response.data);
127
+ },
128
+ paginator: snowflakeGetPaginator,
129
+ ...getterOptions,
130
+ },
131
+ );
132
+
133
+ if (returnGetter) {
134
+ return getter;
135
+ }
136
+
137
+ const data = await getter.run({ returnAll: true });
138
+
139
+ return {
140
+ ok: true,
141
+ data,
142
+ };
143
+ };
144
+
145
+ const funcApiConfig = {
146
+ argsWarden,
147
+ };
148
+
149
+ module.exports = {
150
+ snowflakeGet: (...args) => snowflakeGet(false, ...args),
151
+ snowflakeGetter: (...args) => snowflakeGet(true, ...args),
152
+ funcApiConfig,
153
+ };
@@ -0,0 +1,59 @@
1
+ // https://docs.snowflake.com/en/developer-guide/snowflake-rest-api/reference/api-integration
2
+ // Named IntegrationsGet (not ApiIntegrations) to match bedrock / avoid awkward export names.
3
+
4
+ const { ArgsWarden } = require('../utils');
5
+ const { credsValidator } = require('../validators');
6
+ const {
7
+ snowflakeGet,
8
+ snowflakeGetter,
9
+ } = require('../snowflake/snowflakeGet');
10
+ const {
11
+ REST_API_VERSION,
12
+ DEFAULT_SHOW_LIMIT,
13
+ } = require('../snowflake/snowflake.constants');
14
+
15
+ const argsWarden = new ArgsWarden([
16
+ ['credsPayload', credsValidator],
17
+ ]);
18
+
19
+ const snowflakeIntegrationsGetImpl = async (
20
+ returnGetter,
21
+ credsPayload,
22
+ {
23
+ like,
24
+ showLimit = DEFAULT_SHOW_LIMIT,
25
+ fromName,
26
+ apiVersion = REST_API_VERSION,
27
+ fetchClient,
28
+ ...getterOptions
29
+ } = {},
30
+ ) => {
31
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
32
+ credsPayload,
33
+ });
34
+ if (rejectResponse) {
35
+ return rejectResponse;
36
+ }
37
+
38
+ const method = returnGetter ? snowflakeGetter : snowflakeGet;
39
+
40
+ return method(credsPayload, `/api/${ apiVersion }/api-integrations`, {
41
+ params: {
42
+ ...(like !== undefined && { like }),
43
+ ...(fromName !== undefined && { fromName }),
44
+ },
45
+ showLimit,
46
+ fetchClient,
47
+ ...getterOptions,
48
+ });
49
+ };
50
+
51
+ const funcApiConfig = {
52
+ argsWarden,
53
+ };
54
+
55
+ module.exports = {
56
+ snowflakeIntegrationsGet: (...args) => snowflakeIntegrationsGetImpl(false, ...args),
57
+ snowflakeIntegrationsGetter: (...args) => snowflakeIntegrationsGetImpl(true, ...args),
58
+ funcApiConfig,
59
+ };
@@ -0,0 +1,227 @@
1
+ // https://docs.snowflake.com/en/developer-guide/sql-api/handling-responses
2
+ // Convenience: execute SQL, poll until done, optionally fetch all partitions,
3
+ // and return rows as objects keyed by column name.
4
+
5
+ const { ArgsWarden, logDeep, wait } = require('../utils');
6
+ const { credsValidator } = require('../validators');
7
+ const {
8
+ snowflakeClient,
9
+ mapRows,
10
+ } = require('../snowflake/snowflake.utils');
11
+ const {
12
+ DEFAULT_POLL_INTERVAL_MS,
13
+ DEFAULT_POLL_MAX_ATTEMPTS,
14
+ } = require('../snowflake/snowflake.constants');
15
+ const { snowflakeStatementExecute } = require('../snowflake/snowflakeStatementExecute');
16
+ const { snowflakeStatementGet } = require('../snowflake/snowflakeStatementGet');
17
+
18
+ const argsWarden = new ArgsWarden([
19
+ ['credsPayload', credsValidator],
20
+ ['statement'],
21
+ ]);
22
+
23
+ const isRunningStatus = (payload) => {
24
+ // 202-style status objects expose statementHandle without resultSetMetaData.data complete.
25
+ if (!payload) {
26
+ return false;
27
+ }
28
+ if (payload.statementStatusUrl && !payload.resultSetMetaData) {
29
+ return true;
30
+ }
31
+ // code 090001 is success; running states use other codes with message.
32
+ const message = (payload.message || '').toLowerCase();
33
+ if (message.includes('asynchronous') || message.includes('in progress')) {
34
+ return true;
35
+ }
36
+ return false;
37
+ };
38
+
39
+ const collectAllPartitions = async (
40
+ credsPayload,
41
+ statementHandle,
42
+ firstResult,
43
+ {
44
+ fetchClient,
45
+ } = {},
46
+ ) => {
47
+ const partitionInfo = firstResult?.resultSetMetaData?.partitionInfo ?? [];
48
+ const allData = [...(firstResult.data || [])];
49
+
50
+ // partition 0 is already in firstResult; fetch 1..n-1
51
+ for (let i = 1; i < partitionInfo.length; i += 1) {
52
+ const partResponse = await snowflakeStatementGet(
53
+ credsPayload,
54
+ statementHandle,
55
+ {
56
+ partition: i,
57
+ fetchClient,
58
+ },
59
+ );
60
+ if (!partResponse.ok) {
61
+ return partResponse;
62
+ }
63
+ allData.push(...(partResponse.data?.data || []));
64
+ }
65
+
66
+ return {
67
+ ok: true,
68
+ data: {
69
+ ...firstResult,
70
+ data: allData,
71
+ },
72
+ };
73
+ };
74
+
75
+ const snowflakeQuery = async (
76
+ credsPayload,
77
+ statement,
78
+ {
79
+ timeout,
80
+ database,
81
+ schema,
82
+ warehouse,
83
+ role,
84
+ bindings,
85
+ parameters,
86
+ pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
87
+ pollMaxAttempts = DEFAULT_POLL_MAX_ATTEMPTS,
88
+ fetchAllPartitions = true,
89
+ asObjects = true,
90
+ fetchClient = snowflakeClient,
91
+ } = {},
92
+ ) => {
93
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
94
+ credsPayload,
95
+ statement,
96
+ });
97
+ if (rejectResponse) {
98
+ return rejectResponse;
99
+ }
100
+
101
+ let result = await snowflakeStatementExecute(credsPayload, statement, {
102
+ timeout,
103
+ database,
104
+ schema,
105
+ warehouse,
106
+ role,
107
+ bindings,
108
+ parameters,
109
+ fetchClient,
110
+ });
111
+
112
+ if (!result.ok) {
113
+ return result;
114
+ }
115
+
116
+ let payload = result.data;
117
+ let attempts = 0;
118
+
119
+ while (isRunningStatus(payload) && attempts < pollMaxAttempts) {
120
+ const handle = payload.statementHandle;
121
+ if (!handle) {
122
+ return {
123
+ ok: false,
124
+ error: {
125
+ code: 'MISSING_STATEMENT_HANDLE',
126
+ message: 'Async response missing statementHandle',
127
+ details: payload,
128
+ },
129
+ };
130
+ }
131
+
132
+ await wait(pollIntervalMs);
133
+ attempts += 1;
134
+
135
+ const statusResponse = await snowflakeStatementGet(
136
+ credsPayload,
137
+ handle,
138
+ { fetchClient },
139
+ );
140
+
141
+ if (!statusResponse.ok) {
142
+ // 202 may still come back as ok depending on status; if hard failure, return.
143
+ return statusResponse;
144
+ }
145
+
146
+ payload = statusResponse.data;
147
+ }
148
+
149
+ if (isRunningStatus(payload)) {
150
+ return {
151
+ ok: false,
152
+ error: {
153
+ code: 'STATEMENT_TIMEOUT',
154
+ message: `Statement still running after ${ pollMaxAttempts } polls`,
155
+ details: payload,
156
+ },
157
+ };
158
+ }
159
+
160
+ // Query failure bodies often arrive as HTTP 422 already handled by execute/get.
161
+ if (payload?.code && String(payload.code) !== '090001' && payload.sqlState && payload.sqlState !== '00000') {
162
+ // Some error payloads still return 200 with failure markers — rare.
163
+ if (payload.message && !payload.resultSetMetaData) {
164
+ logDeep({ payload });
165
+ return {
166
+ ok: false,
167
+ error: {
168
+ code: payload.code || 'STATEMENT_FAILED',
169
+ message: payload.message,
170
+ details: payload,
171
+ },
172
+ };
173
+ }
174
+ }
175
+
176
+ const handle = payload.statementHandle;
177
+ let finalPayload = payload;
178
+
179
+ if (fetchAllPartitions && handle) {
180
+ const collected = await collectAllPartitions(
181
+ credsPayload,
182
+ handle,
183
+ payload,
184
+ { fetchClient },
185
+ );
186
+ if (!collected.ok) {
187
+ return collected;
188
+ }
189
+ finalPayload = collected.data;
190
+ }
191
+
192
+ const rows = asObjects
193
+ ? mapRows(finalPayload)
194
+ : (finalPayload.data ?? []);
195
+
196
+ return {
197
+ ok: true,
198
+ data: rows,
199
+ meta: {
200
+ statementHandle: handle,
201
+ numRows: finalPayload?.resultSetMetaData?.numRows
202
+ ?? rows.length,
203
+ rowType: finalPayload?.resultSetMetaData?.rowType,
204
+ sqlState: finalPayload?.sqlState,
205
+ code: finalPayload?.code,
206
+ message: finalPayload?.message,
207
+ },
208
+ };
209
+ };
210
+
211
+ const funcApiConfig = {
212
+ argsWarden,
213
+ };
214
+
215
+ module.exports = {
216
+ snowflakeQuery,
217
+ funcApiConfig,
218
+ };
219
+
220
+ /*
221
+ curl -X POST "http://localhost:8000/snowflakeQuery" \
222
+ -H "Content-Type: application/json" \
223
+ -d '{
224
+ "credsPayload": { "credsPath": "snowflake" },
225
+ "statement": "select 1 as n, current_timestamp() as ts"
226
+ }'
227
+ */
@@ -0,0 +1,69 @@
1
+ // https://docs.snowflake.com/en/developer-guide/snowflake-rest-api/reference/schema
2
+
3
+ const { ArgsWarden } = require('../utils');
4
+ const { credsValidator } = require('../validators');
5
+ const {
6
+ snowflakeGet,
7
+ snowflakeGetter,
8
+ } = require('../snowflake/snowflakeGet');
9
+ const {
10
+ REST_API_VERSION,
11
+ DEFAULT_SHOW_LIMIT,
12
+ } = require('../snowflake/snowflake.constants');
13
+
14
+ const argsWarden = new ArgsWarden([
15
+ ['credsPayload', credsValidator],
16
+ ['databaseName'],
17
+ ]);
18
+
19
+ const snowflakeSchemasGetImpl = async (
20
+ returnGetter,
21
+ credsPayload,
22
+ databaseName,
23
+ {
24
+ like,
25
+ startsWith,
26
+ history,
27
+ showLimit = DEFAULT_SHOW_LIMIT,
28
+ fromName,
29
+ apiVersion = REST_API_VERSION,
30
+ fetchClient,
31
+ ...getterOptions
32
+ } = {},
33
+ ) => {
34
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
35
+ credsPayload,
36
+ databaseName,
37
+ });
38
+ if (rejectResponse) {
39
+ return rejectResponse;
40
+ }
41
+
42
+ const method = returnGetter ? snowflakeGetter : snowflakeGet;
43
+
44
+ return method(
45
+ credsPayload,
46
+ `/api/${ apiVersion }/databases/${ databaseName }/schemas`,
47
+ {
48
+ params: {
49
+ ...(like !== undefined && { like }),
50
+ ...(startsWith !== undefined && { startsWith }),
51
+ ...(history !== undefined && { history }),
52
+ ...(fromName !== undefined && { fromName }),
53
+ },
54
+ showLimit,
55
+ fetchClient,
56
+ ...getterOptions,
57
+ },
58
+ );
59
+ };
60
+
61
+ const funcApiConfig = {
62
+ argsWarden,
63
+ };
64
+
65
+ module.exports = {
66
+ snowflakeSchemasGet: (...args) => snowflakeSchemasGetImpl(false, ...args),
67
+ snowflakeSchemasGetter: (...args) => snowflakeSchemasGetImpl(true, ...args),
68
+ funcApiConfig,
69
+ };
@@ -0,0 +1,75 @@
1
+ // https://docs.snowflake.com/en/developer-guide/sql-api/cancelling-requests
2
+
3
+ const { ArgsWarden, actionSingleOrMultiple, logDeep } = require('../utils');
4
+ const { credsValidator } = require('../validators');
5
+ const { snowflakeClient } = require('../snowflake/snowflake.utils');
6
+ const { SQL_API_PATH } = require('../snowflake/snowflake.constants');
7
+
8
+ const argsWarden = new ArgsWarden([
9
+ ['credsPayload', credsValidator],
10
+ ['statementHandle'],
11
+ ]);
12
+
13
+ const snowflakeStatementCancelSingle = async (
14
+ credsPayload,
15
+ statementHandle,
16
+ {
17
+ fetchClient = snowflakeClient,
18
+ } = {},
19
+ ) => {
20
+ const response = await fetchClient.fetch({
21
+ context: { credsPayload },
22
+ requestPayload: {
23
+ method: 'post',
24
+ url: `${ SQL_API_PATH }/${ statementHandle }/cancel`,
25
+ },
26
+ });
27
+
28
+ const { ok, data, error } = response;
29
+ if (!ok) {
30
+ logDeep({ error });
31
+ return { ok: false, error };
32
+ }
33
+
34
+ return {
35
+ ok: true,
36
+ data: data ?? null,
37
+ };
38
+ };
39
+
40
+ const snowflakeStatementCancel = async (
41
+ credsPayload,
42
+ statementHandle,
43
+ {
44
+ queueRunOptions,
45
+ fetchClient,
46
+ } = {},
47
+ ) => {
48
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
49
+ credsPayload,
50
+ statementHandle,
51
+ });
52
+ if (rejectResponse) {
53
+ return rejectResponse;
54
+ }
55
+
56
+ return actionSingleOrMultiple(
57
+ statementHandle,
58
+ snowflakeStatementCancelSingle,
59
+ (handleItem) => ({
60
+ args: [credsPayload, handleItem, { fetchClient }],
61
+ }),
62
+ {
63
+ ...(queueRunOptions ? { queueRunOptions } : {}),
64
+ },
65
+ );
66
+ };
67
+
68
+ const funcApiConfig = {
69
+ argsWarden,
70
+ };
71
+
72
+ module.exports = {
73
+ snowflakeStatementCancel,
74
+ funcApiConfig,
75
+ };
@@ -0,0 +1,105 @@
1
+ // https://docs.snowflake.com/en/developer-guide/sql-api/submitting-requests
2
+
3
+ const { randomUUID } = require('crypto');
4
+
5
+ const { ArgsWarden, logDeep } = require('../utils');
6
+ const { credsValidator } = require('../validators');
7
+ const {
8
+ snowflakeClient,
9
+ sessionContextFromCreds,
10
+ } = require('../snowflake/snowflake.utils');
11
+ const {
12
+ SQL_API_PATH,
13
+ DEFAULT_STATEMENT_TIMEOUT_SECONDS,
14
+ } = require('../snowflake/snowflake.constants');
15
+
16
+ const argsWarden = new ArgsWarden([
17
+ ['credsPayload', credsValidator],
18
+ ['statement'],
19
+ ]);
20
+
21
+ const snowflakeStatementExecute = async (
22
+ credsPayload,
23
+ statement,
24
+ {
25
+ timeout = DEFAULT_STATEMENT_TIMEOUT_SECONDS,
26
+ database,
27
+ schema,
28
+ warehouse,
29
+ role,
30
+ bindings,
31
+ parameters,
32
+ async: asAsync = false,
33
+ nullable,
34
+ requestId,
35
+ fetchClient = snowflakeClient,
36
+ } = {},
37
+ ) => {
38
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
39
+ credsPayload,
40
+ statement,
41
+ });
42
+ if (rejectResponse) {
43
+ return rejectResponse;
44
+ }
45
+
46
+ // resolveCreds runs in the client; session defaults come from creds when present.
47
+ const { credsFromPayload } = require('../utils');
48
+ const creds = await credsFromPayload(credsPayload);
49
+
50
+ const body = {
51
+ statement,
52
+ timeout,
53
+ ...sessionContextFromCreds(creds, {
54
+ database,
55
+ schema,
56
+ warehouse,
57
+ role,
58
+ }),
59
+ ...(bindings !== undefined && { bindings }),
60
+ ...(parameters !== undefined && { parameters }),
61
+ };
62
+
63
+ const response = await fetchClient.fetch({
64
+ context: { credsPayload },
65
+ requestPayload: {
66
+ method: 'post',
67
+ url: SQL_API_PATH,
68
+ params: {
69
+ requestId: requestId || randomUUID(),
70
+ ...(asAsync ? { async: true } : {}),
71
+ ...(nullable !== undefined && { nullable }),
72
+ },
73
+ body,
74
+ },
75
+ });
76
+
77
+ const { ok, data, error } = response;
78
+ if (!ok) {
79
+ logDeep({ error });
80
+ return { ok: false, error };
81
+ }
82
+
83
+ return {
84
+ ok: true,
85
+ data,
86
+ };
87
+ };
88
+
89
+ const funcApiConfig = {
90
+ argsWarden,
91
+ };
92
+
93
+ module.exports = {
94
+ snowflakeStatementExecute,
95
+ funcApiConfig,
96
+ };
97
+
98
+ /*
99
+ curl -X POST "http://localhost:8000/snowflakeStatementExecute" \
100
+ -H "Content-Type: application/json" \
101
+ -d '{
102
+ "credsPayload": { "credsPath": "snowflake" },
103
+ "statement": "select current_version() as version"
104
+ }'
105
+ */