@foxtware/mineral 1.0.2 → 1.0.3

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.
@@ -1,8 +1,11 @@
1
- const { ArgsWarden, objHasAny } = require('../utils');
1
+ const { ArgsWarden, arrayToChunks, objHasAny } = require('../utils');
2
2
  const { credsValidator } = require('../validators');
3
3
  const { getGoogleSheets, googleApiCall } = require('../google/google.utils');
4
4
  const { googlesheetsSpreadsheetTrim } = require('../google/googlesheetsSpreadsheetTrim');
5
5
 
6
+ // Sheets rejects oversized request bodies, so big exports go up in slices.
7
+ const MAX_ROWS_PER_UPDATE = 5000;
8
+
6
9
  const spreadsheetIdentifierValidator = (spreadsheetIdentifier) => {
7
10
  return objHasAny(spreadsheetIdentifier, ['spreadsheetId']);
8
11
  };
@@ -81,6 +84,10 @@ const googlesheetsSpreadsheetSheetAdd = async (
81
84
  addSheet: {
82
85
  properties: {
83
86
  title: String(sheetName),
87
+ gridProperties: {
88
+ rowCount: values.length,
89
+ columnCount: headers.length,
90
+ },
84
91
  },
85
92
  },
86
93
  },
@@ -94,17 +101,23 @@ const googlesheetsSpreadsheetSheetAdd = async (
94
101
 
95
102
  const newSheetId = batchUpdateResponse.data.replies[0].addSheet.properties.sheetId;
96
103
 
97
- const updateResponse = await googleApiCall(() => client.spreadsheets.values.update({
98
- spreadsheetId,
99
- range: `${ sheetName }!A1`,
100
- valueInputOption: 'RAW',
101
- requestBody: {
102
- values,
103
- },
104
- }));
104
+ const chunks = arrayToChunks(values, MAX_ROWS_PER_UPDATE);
105
105
 
106
- if (!updateResponse.ok) {
107
- return updateResponse;
106
+ let updateResponse;
107
+
108
+ for (const [chunkIndex, chunk] of chunks.entries()) {
109
+ updateResponse = await googleApiCall(() => client.spreadsheets.values.update({
110
+ spreadsheetId,
111
+ range: `${ sheetName }!A${ (chunkIndex * MAX_ROWS_PER_UPDATE) + 1 }`,
112
+ valueInputOption: 'RAW',
113
+ requestBody: {
114
+ values: chunk,
115
+ },
116
+ }));
117
+
118
+ if (!updateResponse.ok) {
119
+ return updateResponse;
120
+ }
108
121
  }
109
122
 
110
123
  if (trim) {
@@ -0,0 +1,157 @@
1
+ // https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#deletesheetrequest
2
+
3
+ const { ArgsWarden, ensureArray, objHasAny } = require('../utils');
4
+ const { credsValidator } = require('../validators');
5
+ const { getGoogleSheets, googleApiCall } = require('../google/google.utils');
6
+
7
+ const spreadsheetIdentifierValidator = (spreadsheetIdentifier) => {
8
+ return objHasAny(spreadsheetIdentifier, ['spreadsheetId']);
9
+ };
10
+
11
+ const sheetIdentifierValidator = (sheetIdentifier) => {
12
+ const sheetIdentifiers = ensureArray(sheetIdentifier);
13
+
14
+ return sheetIdentifiers.length > 0
15
+ && sheetIdentifiers.every((identifier) => {
16
+ return objHasAny(identifier, ['sheetName', 'sheetId']);
17
+ });
18
+ };
19
+
20
+ const argsWarden = new ArgsWarden([
21
+ ['credsPayload', credsValidator],
22
+ ['spreadsheetIdentifier', spreadsheetIdentifierValidator],
23
+ ['sheetIdentifier', sheetIdentifierValidator],
24
+ ]);
25
+
26
+ const googlesheetsSpreadsheetSheetDelete = async (
27
+ credsPayload,
28
+ spreadsheetIdentifier,
29
+ sheetIdentifier,
30
+ {
31
+ missingOk = true,
32
+ } = {},
33
+ ) => {
34
+
35
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
36
+ credsPayload,
37
+ spreadsheetIdentifier,
38
+ sheetIdentifier,
39
+ });
40
+ if (rejectResponse) {
41
+ return rejectResponse;
42
+ }
43
+
44
+ const { spreadsheetId } = spreadsheetIdentifier;
45
+ const sheetIdentifiers = ensureArray(sheetIdentifier);
46
+
47
+ const { client, error } = await getGoogleSheets(credsPayload);
48
+
49
+ if (error) {
50
+ return error;
51
+ }
52
+
53
+ const spreadsheetResponse = await googleApiCall(() => client.spreadsheets.get({
54
+ spreadsheetId,
55
+ }));
56
+
57
+ if (!spreadsheetResponse.ok) {
58
+ return spreadsheetResponse;
59
+ }
60
+
61
+ const sheetsArray = spreadsheetResponse.data?.sheets || [];
62
+
63
+ const resolved = [];
64
+ const missing = [];
65
+
66
+ for (const { sheetName, sheetId } of sheetIdentifiers) {
67
+ const sheet = sheetsArray.find(({ properties }) => {
68
+ return sheetId !== undefined
69
+ ? properties.sheetId === sheetId
70
+ : properties.title === sheetName;
71
+ });
72
+
73
+ if (!sheet) {
74
+ missing.push({ sheetName, sheetId });
75
+ continue;
76
+ }
77
+
78
+ resolved.push({
79
+ sheetId: sheet.properties.sheetId,
80
+ sheetName: sheet.properties.title,
81
+ });
82
+ }
83
+
84
+ if (missing.length && !missingOk) {
85
+ return {
86
+ ok: false,
87
+ error: {
88
+ code: 'SHEET_NOT_FOUND',
89
+ message: `Sheet not found: ${ JSON.stringify(missing) }`,
90
+ },
91
+ };
92
+ }
93
+
94
+ // Sheets refuses to remove the last remaining tab, so say so plainly rather
95
+ // than letting the API error surface without context.
96
+ if (resolved.length && resolved.length === sheetsArray.length) {
97
+ return {
98
+ ok: false,
99
+ error: {
100
+ code: 'CANNOT_DELETE_ALL_SHEETS',
101
+ message: 'A spreadsheet must keep at least one sheet',
102
+ },
103
+ };
104
+ }
105
+
106
+ if (!resolved.length) {
107
+ return {
108
+ ok: true,
109
+ data: {
110
+ deleted: [],
111
+ missing,
112
+ },
113
+ };
114
+ }
115
+
116
+ const batchUpdateResponse = await googleApiCall(() => client.spreadsheets.batchUpdate({
117
+ spreadsheetId,
118
+ requestBody: {
119
+ requests: resolved.map(({ sheetId }) => ({
120
+ deleteSheet: {
121
+ sheetId,
122
+ },
123
+ })),
124
+ },
125
+ }));
126
+
127
+ if (!batchUpdateResponse.ok) {
128
+ return batchUpdateResponse;
129
+ }
130
+
131
+ return {
132
+ ok: true,
133
+ data: {
134
+ deleted: resolved,
135
+ missing,
136
+ },
137
+ };
138
+ };
139
+
140
+ const funcApiConfig = {
141
+ argsWarden,
142
+ };
143
+
144
+ module.exports = {
145
+ googlesheetsSpreadsheetSheetDelete,
146
+ funcApiConfig,
147
+ };
148
+
149
+ /*
150
+ curl -X POST "http://localhost:8000/googlesheetsSpreadsheetSheetDelete" \
151
+ -H "Content-Type: application/json" \
152
+ -d '{
153
+ "credsPayload": { "credsPath": "google" },
154
+ "spreadsheetIdentifier": { "spreadsheetId": "ABC123" },
155
+ "sheetIdentifier": [{ "sheetName": "AU" }, { "sheetName": "US" }]
156
+ }'
157
+ */
@@ -17,6 +17,8 @@ const googlesheetsSpreadsheetTrim = async (
17
17
  {
18
18
  } = {},
19
19
  ) => {
20
+ const { spreadsheetId } = spreadsheetIdentifier;
21
+
20
22
  const { client, error } = await getGoogleSheets(credsPayload);
21
23
 
22
24
  if (error) {
@@ -0,0 +1,114 @@
1
+ // https://linear.app/developers/graphql
2
+
3
+ const { ArgsWarden, actionSingleOrMultiple, everyIfArray, valueProvided } = require('../utils');
4
+ const { credsValidator } = require('../validators');
5
+ const { linearClient } = require('../linear/linear.utils');
6
+
7
+ const invitePayloadValidator = (invitePayload) => {
8
+ return valueProvided(invitePayload?.email);
9
+ };
10
+
11
+ const argsWarden = new ArgsWarden([
12
+ ['credsPayload', credsValidator],
13
+ ['invitePayload', (invitePayload) => everyIfArray(invitePayloadValidator, invitePayload)],
14
+ ]);
15
+
16
+ const linearOrganizationInviteCreateSingle = async (
17
+ credsPayload,
18
+ invitePayload,
19
+ {
20
+ inspect = false,
21
+ fetchClient = linearClient,
22
+ } = {},
23
+ ) => {
24
+ const {
25
+ email,
26
+ role = 'user',
27
+ teamIds,
28
+ } = invitePayload;
29
+
30
+ return fetchClient.fetch({
31
+ requestPayload: {
32
+ body: {
33
+ query: `
34
+ mutation OrganizationInviteCreate($input: OrganizationInviteCreateInput!) {
35
+ organizationInviteCreate(input: $input) {
36
+ success
37
+ organizationInvite {
38
+ id
39
+ email
40
+ role
41
+ createdAt
42
+ expiresAt
43
+ acceptedAt
44
+ }
45
+ }
46
+ }
47
+ `,
48
+ variables: {
49
+ input: {
50
+ email,
51
+ role,
52
+ ...teamIds && { teamIds },
53
+ },
54
+ },
55
+ },
56
+ },
57
+ context: {
58
+ credsPayload,
59
+ resultPath: 'data.organizationInviteCreate',
60
+ },
61
+ inspect,
62
+ });
63
+ };
64
+
65
+ const linearOrganizationInviteCreate = async (
66
+ credsPayload,
67
+ invitePayload,
68
+ {
69
+ inspect = false,
70
+ fetchClient = linearClient,
71
+ queueRunOptions,
72
+ } = {},
73
+ ) => {
74
+
75
+ const rejectResponse = await argsWarden.responseIfRejectingArgs({
76
+ credsPayload,
77
+ invitePayload,
78
+ });
79
+ if (rejectResponse) {
80
+ return rejectResponse;
81
+ }
82
+
83
+ return actionSingleOrMultiple(
84
+ invitePayload,
85
+ linearOrganizationInviteCreateSingle,
86
+ (invitePayloadItem) => ({
87
+ args: [credsPayload, invitePayloadItem, { inspect, fetchClient }],
88
+ }),
89
+ {
90
+ ...(queueRunOptions ? { queueRunOptions } : {}),
91
+ },
92
+ );
93
+ };
94
+
95
+ const funcApiConfig = {
96
+ argsWarden,
97
+ };
98
+
99
+ module.exports = {
100
+ linearOrganizationInviteCreate,
101
+ funcApiConfig,
102
+ };
103
+
104
+ /*
105
+ curl -X POST "http://localhost:8000/linearOrganizationInviteCreate" \
106
+ -H "Content-Type: application/json" \
107
+ -d '{
108
+ "credsPayload": { "credsPath": "linear" },
109
+ "invitePayload": [
110
+ { "email": "someone@whitefoxboutique.com" },
111
+ { "email": "someoneelse@whitefoxboutique.com" }
112
+ ]
113
+ }'
114
+ */
package/api/workspace.js CHANGED
@@ -32,6 +32,7 @@ const loadWorkspaceEnv = () => {
32
32
  dotenv.config({
33
33
  path: envFile,
34
34
  override: true,
35
+ quiet: true,
35
36
  });
36
37
  };
37
38
 
@@ -16,6 +16,7 @@ const {
16
16
 
17
17
  dotenv.config({
18
18
  path: path.join(process.cwd(), '.env'),
19
+ quiet: true,
19
20
  });
20
21
 
21
22
  const MINERAL_WRAPPERS_MODULE = '@foxtware/mineral/hosting/wrappers.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxtware/mineral",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "bin": {
5
5
  "mineral": "bin/mineral.js"
6
6
  },