@velocitycareerlabs/data-loader 1.20.0-dev-build.1cb592b8f → 1.20.0-dev-build.1e6832e17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velocitycareerlabs/data-loader",
3
- "version": "1.20.0-dev-build.1cb592b8f",
3
+ "version": "1.20.0-dev-build.1e6832e17",
4
4
  "description": "A tool for uploading data to the different target systems.",
5
5
  "repository": "https://github.com/velocitycareerlabs/packages",
6
6
  "main": "src/index.js",
@@ -26,6 +26,7 @@
26
26
  "eslint-plugin-prettier": "^4.2.1",
27
27
  "eslint-watch": "^7.0.0",
28
28
  "jest": "29.6.2",
29
+ "nock": "^13.2.9",
29
30
  "prettier": "^2.2.1"
30
31
  },
31
32
  "dependencies": {
@@ -39,5 +40,5 @@
39
40
  "lodash": "~4.17.20",
40
41
  "nanoid": "3.3.1"
41
42
  },
42
- "gitHead": "1d9b55e87dc8ed0df48d736d72ca68d5841a10ff"
43
+ "gitHead": "f137575a659703ef89f3cd454989c46d0e0dc231"
43
44
  }
@@ -3,6 +3,31 @@ const DisclosureType = {
3
3
  EXISTING: 'existing',
4
4
  };
5
5
 
6
+ const CREDENTIAL_TYPES = {
7
+ EMAIL: 'EmailV1.0',
8
+ PHONE: 'PhoneV1.0',
9
+ DRIVER_LICENSE: 'DriversLicenseV1.0',
10
+ ID_DOCUMENT: 'IdDocumentV1.0',
11
+ NATIONAL_ID_CARD: 'NationalIdCardV1.0',
12
+ PASSPORT: 'PassportV1.0',
13
+ PROOF_OF_AGE: 'ProofOfAgeV1.0',
14
+ RESIDENT_PERMIT: 'ResidentPermitV1.0',
15
+ };
16
+ const DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH =
17
+ '$.idDocumentCredentials[*].credentialSubject.identifier';
18
+ const idCredentialTypeToPick = {
19
+ [CREDENTIAL_TYPES.EMAIL]: '$.emails',
20
+ [CREDENTIAL_TYPES.PHONE]: '$.phones',
21
+ [CREDENTIAL_TYPES.DRIVER_LICENSE]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
22
+ [CREDENTIAL_TYPES.ID_DOCUMENT]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
23
+ [CREDENTIAL_TYPES.NATIONAL_ID_CARD]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
24
+ [CREDENTIAL_TYPES.PASSPORT]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
25
+ [CREDENTIAL_TYPES.PROOF_OF_AGE]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
26
+ [CREDENTIAL_TYPES.RESIDENT_PERMIT]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
27
+ };
28
+
6
29
  module.exports = {
7
30
  DisclosureType,
31
+ CREDENTIAL_TYPES,
32
+ idCredentialTypeToPick,
8
33
  };
@@ -18,14 +18,11 @@ const writeJsonFile = async (obj, jsonName, { path }) => {
18
18
  return { fileName, filePath };
19
19
  };
20
20
 
21
- const writeOutputCsv = (
22
- data,
23
- { path, outputCsvName = 'output', vendorUserIdName = 'email' }
24
- ) => {
21
+ const writeOutputCsv = (data, { path, outputCsvName, vendorUserIdColName }) => {
25
22
  const fileName = `${outputCsvName}.csv`;
26
23
  const filePath = join(path, fileName);
27
24
  const buff = fs.createWriteStream(filePath, {});
28
- buff.write([vendorUserIdName, ...OUTPUT_CSV_HEADERS].join(','));
25
+ buff.write([vendorUserIdColName, ...OUTPUT_CSV_HEADERS].join(','));
29
26
  for (const { vendorUserId, deeplink, qrcodeFilePath } of data) {
30
27
  buff.write(`\n${vendorUserId},${deeplink},${qrcodeFilePath}`);
31
28
  }
@@ -1,79 +1,60 @@
1
1
  const { program } = require('commander');
2
2
  const { reduce } = require('lodash/fp');
3
- const { printInfo, printError } = require('../helpers/common');
3
+ const { printInfo, printError, parseColumn } = require('../helpers');
4
4
  const { runBatchIssuing } = require('./orchestrators');
5
5
 
6
- const parseVar = reduce((acc, pair) => {
7
- const [key, value] = pair.split('=');
8
- acc[key] = value;
9
- return acc;
10
- }, {});
11
-
12
- const validateOptions = (options) => {
13
- if (options.dryrun == null && options.endpoint == null) {
14
- throw new Error(
15
- '"-e" or "--endpoint" is required unless executing a "dryrun"'
16
- );
17
- }
18
- if (options.endpoint != null && options.authToken == null) {
19
- throw new Error('"-a" or "--auth-token" is required');
20
- }
21
- };
22
-
23
6
  program
24
7
  .name('data-loader vendorcreds')
25
8
  .description('Loads data into a db')
26
9
  .usage('[options]')
27
10
  .requiredOption(
28
11
  '-c, --csv-filename <filename>',
29
- 'File name containing variables'
12
+ 'file name containing variables'
30
13
  )
31
14
  .requiredOption(
32
15
  '-o, --offer-template-filename <filename>',
33
- 'File name containing the credential template file'
34
- )
35
- .requiredOption(
36
- '-d, --did <did>',
37
- 'DID of the organization that should do the issuing'
16
+ 'file name containing the credential template file'
38
17
  )
18
+ .requiredOption('-d, --did <did>', "the issuer's DID")
39
19
  .requiredOption(
40
20
  '-p, --path <path>',
41
- 'path of directory for the qr-code images and output CSV'
21
+ 'the output directory to use where QR codes and output state files are stored'
42
22
  )
43
23
  .requiredOption(
44
24
  '-t, --terms-url <termsUrl>',
45
- 'The terms and conditions url for the holder to accept'
25
+ 'the url to the T&Cs that holder must consent to'
46
26
  )
47
27
  .option(
48
- '--legacy',
49
- 'the target credential agent is running in the "LEGACY" offer type mode. Default is false'
28
+ '-m --identifier-match-column <identifierMatchColumn>',
29
+ `the column from the CSV for the user to be matched against the ID credential's "identifier" property
30
+ For example this should be the email column if matching against an Email credential type, or the phone number if
31
+ matching against a Phone credential type. Accepts header name or index. Default is 0.`,
32
+ parseColumn,
33
+ '0'
50
34
  )
51
35
  .option(
52
- '-x --outputcsv',
53
- "if passed an output csv is generated including the vendor's user id as the first column and the generated qrcode filename and deeplink"
36
+ '-u --vendor-userid-column <vendorUseridColumn>',
37
+ `the column from the CSV that is users id. Value is made available as "vendorUserId" in the offer template. Accepts
38
+ header name or index. Default is 0.`,
39
+ parseColumn,
40
+ '0'
54
41
  )
55
42
  .option(
56
- '--x-name <outputCsvName>',
57
- 'The file name for the output CSV. Default is "output"'
43
+ '-e, --endpoint <url>',
44
+ 'Credential Agent Endpoint to call to execute the issuing'
58
45
  )
59
46
  .option(
60
- '--x-vendor-userid-name <vendorUserIdName>',
61
- 'The column name to use for the vendor user id in the output csv. Default is "email"'
47
+ '-a, --auth-token <url>',
48
+ 'Bearer Auth Token to be used on the Agent API'
62
49
  )
50
+ .option('-l, --label <label>', 'A label to attach to the documents inserted')
63
51
  .option(
64
- '--dryrun',
65
- 'if passed in then a dry run executes showing how the data will be formatted'
52
+ '-v, --var <var...>',
53
+ 'A variable that will be injected into the credential template renderer. use name=value'
66
54
  )
67
55
  .option(
68
- '-e, --endpoint <url>',
69
- 'Credential Agent Endpoint to call to execute the issuing'
70
- )
71
- .option('-a, --auth-token <url>', 'Bearer Auth Token to use')
72
- .option('-l, --label <label>', 'A label to attach to the documents inserted')
73
- .option('-v, --var <var...>', 'A variable to add. use name=value')
74
- .option(
75
- '-y, --credential-type <credentialType>',
76
- 'The type of credentials. Default is EmailV1.0.',
56
+ '-y, --credential-type <idCredentialType>',
57
+ 'the credential type used for identifying the user. Default is EmailV1.0.',
77
58
  'EmailV1.0'
78
59
  )
79
60
  .option(
@@ -82,17 +63,32 @@ program
82
63
  )
83
64
  .option(
84
65
  '--authTokenExpiresIn <authTokenExpiresIn>',
85
- 'The number of minutes that the offer will be available for after activation. Default is 90 days.',
86
- '10080'
66
+ 'The number of minutes that the offer will be available for after activation. Default is 365 days.',
67
+ '525600'
87
68
  )
88
69
  .option('--new', 'Use a new disclosure for batch issuing')
89
70
  .option(
90
71
  '-i, --disclosure [disclosure]',
91
72
  'An existing disclosure to use for the batch issuing'
92
73
  )
74
+ .option(
75
+ '--legacy',
76
+ 'the target credential agent is running in the "LEGACY" offer type mode. Default is false'
77
+ )
78
+ .option(
79
+ '-x --outputcsv',
80
+ "if passed an output csv is generated including the vendor's user id as the first column and the generated qrcode filename and deeplink"
81
+ )
82
+ .option(
83
+ '--x-name <outputCsvName>',
84
+ 'The file name for the output CSV. Default is "output"'
85
+ )
86
+ .option(
87
+ '--dryrun',
88
+ 'if passed in then a dry run executes showing how the data will be formatted'
89
+ )
93
90
  .action(async () => {
94
91
  const options = program.opts();
95
- validateOptions(options);
96
92
  // eslint-disable-next-line better-mutation/no-mutation
97
93
  options.vars = parseVar(options.var);
98
94
  printInfo(options);
@@ -113,3 +109,9 @@ program
113
109
  }
114
110
  })
115
111
  .parseAsync(process.argv);
112
+
113
+ const parseVar = reduce((acc, pair) => {
114
+ const [key, value] = pair.split('=');
115
+ acc[key] = value;
116
+ return acc;
117
+ }, {});
@@ -1,93 +1,117 @@
1
- const { omitBy, isNil, find, isString, isEmpty } = require('lodash/fp');
1
+ const { omitBy, isNil, find, isString, isEmpty, values } = require('lodash/fp');
2
2
  const { validateDirectoryExists } = require('./validate-directory-exists');
3
3
  const { initFetchers } = require('./fetchers');
4
- const { prepareData } = require('./prepare-data');
4
+ const {
5
+ prepareNewDisclosureRequest,
6
+ prepareExchangeOffers,
7
+ } = require('./prepare-data');
5
8
  const { printInfo } = require('../helpers/common');
6
9
  const {
7
10
  writeQrCodeFile,
8
11
  writeJsonFile,
9
12
  writeOutputCsv,
10
13
  } = require('./file-writers');
11
- const { DisclosureType } = require('./constants');
14
+ const { CREDENTIAL_TYPES, DisclosureType } = require('./constants');
12
15
  const {
13
16
  askDisclosureType,
14
17
  askDisclosureList,
15
18
  askUseNewDisclosure,
16
19
  } = require('./prompts');
20
+ const { loadCsv, getColName } = require('../helpers/load-csv');
17
21
 
18
- // eslint-disable-next-line complexity
19
- const getDisclosureType = async (hasIntegratedDisclosures, options) => {
20
- const isIntegratedMode = !options.new && !options.disclosure;
21
-
22
- switch (true) {
23
- case isIntegratedMode && !hasIntegratedDisclosures: {
24
- const useNewDisclosure = await askUseNewDisclosure();
25
- return useNewDisclosure ? DisclosureType.NEW : null;
26
- }
27
- case isIntegratedMode && hasIntegratedDisclosures:
28
- return askDisclosureType();
29
- case Boolean(options.disclosure):
30
- return DisclosureType.EXISTING;
31
- default:
32
- return DisclosureType.NEW;
33
- }
22
+ const defaultOptions = {
23
+ vendorUseridColumn: 0,
24
+ identifierMatchColumn: 0,
25
+ authTokenExpiresIn: 525600,
26
+ legacy: false,
27
+ outputCsvName: 'output',
28
+ idCredentialType: 'EmailV1.0',
34
29
  };
35
30
 
36
- const runBatchIssuing = async (options) => {
37
- validateDirectoryExists(options);
38
- const fetchers = initFetchers(options);
39
- const disclosures = await getIntegratedDisclosures(fetchers);
40
- const disclosureType = await getDisclosureType(
41
- !isEmpty(disclosures),
42
- options
43
- );
31
+ // eslint-disable-next-line consistent-return
32
+ const runBatchIssuing = async (opts) => {
33
+ const options = { ...defaultOptions, ...opts };
34
+ validateOptions(options);
44
35
 
45
- if (isEmpty(disclosureType)) {
46
- return;
47
- }
36
+ const context = { fetchers: initFetchers(options) };
48
37
 
49
- const { newDisclosureRequest, newExchangeOffers } = await prepareData(
50
- disclosureType,
38
+ const [csvHeaders, csvRows] = await loadCsv(options.csvFilename);
39
+
40
+ const disclosureRequest = await loadOrPrepareNewDisclosureRequest(
41
+ csvHeaders,
42
+ options,
43
+ context
44
+ );
45
+
46
+ const newExchangeOffers = await prepareExchangeOffers(
47
+ csvHeaders,
48
+ csvRows,
51
49
  options
52
50
  );
53
51
 
54
- if (options.endpoint == null) {
52
+ if (options.dryrun) {
55
53
  printInfo('Dry Run would create:');
56
54
  printInfo(
57
55
  JSON.stringify(
58
- omitBy(isNil, { newDisclosureRequest, newExchangeOffers }),
56
+ omitBy(isNil, { disclosureRequest, newExchangeOffers }),
59
57
  0,
60
58
  2
61
59
  )
62
60
  );
63
- return;
61
+
62
+ return { disclosureRequest, newExchangeOffers };
64
63
  }
65
64
 
66
- const disclosure = await createOrGotDisclosure({
67
- disclosureType,
68
- disclosures,
69
- newDisclosureRequest,
70
- fetchers,
71
- options,
72
- });
65
+ if (disclosureRequest.id == null) {
66
+ await createDisclosureRequest(disclosureRequest, context);
67
+ }
73
68
 
74
- const disclosureRequest = await writeDisclosureToJson(disclosure, options);
69
+ await writeDisclosureToJson(disclosureRequest, options);
75
70
 
76
71
  const outputs = options.legacy
77
72
  ? await runLegacyBatchIssuing(
78
73
  disclosureRequest,
79
74
  newExchangeOffers,
80
- fetchers,
81
- options
75
+ options,
76
+ context
82
77
  )
83
78
  : await runSingleQrCodeBatchIssuing(
84
79
  disclosureRequest,
85
80
  newExchangeOffers,
86
- fetchers,
87
- options
81
+ options,
82
+ context
88
83
  );
89
84
 
90
- writeOutput(outputs, options);
85
+ writeOutput(outputs, {
86
+ ...options,
87
+ vendorUserIdColName: getColName(csvHeaders, options.vendorUseridColumn),
88
+ });
89
+ };
90
+
91
+ const loadExistingDisclosuresIfRequired = async (options, context) => {
92
+ if (options.new) {
93
+ return [];
94
+ }
95
+
96
+ const disclosures = await loadIntegratedIdentificationDisclosures(context);
97
+ if (options.disclosure) {
98
+ return disclosures;
99
+ }
100
+
101
+ // interactive mode is handled below
102
+
103
+ if (isEmpty(disclosures)) {
104
+ const useNewDisclosure = await askUseNewDisclosure();
105
+ if (!useNewDisclosure)
106
+ throw new Error(
107
+ 'no existing disclosures on the target agent. Use a new disclosure'
108
+ );
109
+
110
+ return [];
111
+ }
112
+
113
+ const disclosureType = await askDisclosureType();
114
+ return disclosureType === DisclosureType.NEW ? [] : disclosures;
91
115
  };
92
116
 
93
117
  const writeOutput = (outputs, options) => {
@@ -99,19 +123,21 @@ const writeOutput = (outputs, options) => {
99
123
  const runLegacyBatchIssuing = async (
100
124
  disclosureRequest,
101
125
  newExchangeOffers,
102
- fetchers,
103
- options
126
+ options,
127
+ context
104
128
  ) => {
105
129
  const outputs = [];
106
130
  for (const newExchangeOffer of newExchangeOffers) {
107
131
  outputs.push(
108
132
  // eslint-disable-next-line no-await-in-loop
109
- await createOfferExchangeAndQrCode({
110
- ...newExchangeOffer,
111
- disclosureRequest,
112
- fetchers,
133
+ await createOfferExchangeAndQrCode(
134
+ {
135
+ ...newExchangeOffer,
136
+ disclosureRequest,
137
+ },
113
138
  options,
114
- })
139
+ context
140
+ )
115
141
  );
116
142
  }
117
143
 
@@ -121,22 +147,24 @@ const runLegacyBatchIssuing = async (
121
147
  const runSingleQrCodeBatchIssuing = async (
122
148
  disclosureRequest,
123
149
  newExchangeOffers,
124
- fetchers,
125
- options
150
+ options,
151
+ context
126
152
  ) => {
127
153
  const outputs = [];
128
154
  for (const newExchangeOffer of newExchangeOffers) {
129
155
  outputs.push(
130
156
  // eslint-disable-next-line no-await-in-loop
131
- await createOfferExchange({
132
- ...newExchangeOffer,
133
- disclosureRequest,
134
- fetchers,
135
- options,
136
- })
157
+ await createOfferExchange(
158
+ {
159
+ ...newExchangeOffer,
160
+ disclosureRequest,
161
+ },
162
+ context
163
+ )
137
164
  );
138
165
  }
139
166
 
167
+ const { fetchers } = context;
140
168
  printInfo('Generating qrcode & deep link');
141
169
  const deeplink = await fetchers.loadDisclosureDeeplink(disclosureRequest);
142
170
  printInfo(`Deep link: ${deeplink}`);
@@ -147,21 +175,32 @@ const runSingleQrCodeBatchIssuing = async (
147
175
  return outputs;
148
176
  };
149
177
 
150
- const getIntegratedDisclosures = async (fetchers) => {
151
- const vendorEndpoints = ['integrated-issuing-identification'];
152
- const disclosures = await fetchers.getDisclosureList(vendorEndpoints);
153
- return disclosures;
154
- };
178
+ const loadOrPrepareNewDisclosureRequest = async (
179
+ csvHeaders,
180
+ options,
181
+ context
182
+ ) => {
183
+ const disclosures = await loadExistingDisclosuresIfRequired(options, context);
155
184
 
156
- const getDisclosure = async (disclosures, fetchers, options) => {
157
- const { disclosure } = options;
185
+ if (isEmpty(disclosures)) {
186
+ return prepareNewDisclosureRequest(csvHeaders, options);
187
+ }
158
188
 
159
- if (isString(disclosure)) {
160
- return fetchers.getDisclosure(disclosure);
189
+ const disclosureId = isString(options.disclosure)
190
+ ? options.disclosure
191
+ : await askDisclosureList(disclosures);
192
+ const disclosure = find({ id: disclosureId }, disclosures);
193
+ if (disclosure == null) {
194
+ throw new Error('existing disclosure not found');
161
195
  }
196
+ return disclosure;
197
+ };
162
198
 
163
- const disclosureId = await askDisclosureList(disclosures);
164
- return find(({ id }) => id === disclosureId, disclosures);
199
+ const loadIntegratedIdentificationDisclosures = async ({ fetchers }) =>
200
+ fetchers.getDisclosureList(['integrated-issuing-identification']);
201
+
202
+ const createDisclosureRequest = async (newDisclosureRequest, { fetchers }) => {
203
+ return fetchers.createDisclosure(newDisclosureRequest);
165
204
  };
166
205
 
167
206
  const writeDisclosureToJson = async (disclosureRequest, options) => {
@@ -183,16 +222,13 @@ const writeDisclosureToJson = async (disclosureRequest, options) => {
183
222
  'lastrun',
184
223
  options
185
224
  );
186
- return disclosureRequest;
187
225
  };
188
226
 
189
- const createOfferExchangeAndQrCode = async ({
190
- newExchange,
191
- newOffer,
192
- disclosureRequest,
227
+ const createOfferExchangeAndQrCode = async (
228
+ { newExchange, newOffer, disclosureRequest },
193
229
  options,
194
- fetchers,
195
- }) => {
230
+ { fetchers }
231
+ ) => {
196
232
  const { exchange, offer, vendorUserId } = await createOfferExchange({
197
233
  newExchange,
198
234
  newOffer,
@@ -220,12 +256,10 @@ const createOfferExchangeAndQrCode = async ({
220
256
  };
221
257
  };
222
258
 
223
- const createOfferExchange = async ({
224
- newExchange,
225
- newOffer,
226
- disclosureRequest,
227
- fetchers,
228
- }) => {
259
+ const createOfferExchange = async (
260
+ { newExchange, newOffer, disclosureRequest },
261
+ { fetchers }
262
+ ) => {
229
263
  const { vendorUserId } = newOffer.credentialSubject;
230
264
  printInfo(`Setting up vendorUserId:${vendorUserId}`);
231
265
  const exchange = await fetchers.createOfferExchange({
@@ -241,18 +275,31 @@ const createOfferExchange = async ({
241
275
  };
242
276
  };
243
277
 
244
- const createOrGotDisclosure = async ({
245
- disclosureType,
246
- disclosures,
247
- newDisclosureRequest,
248
- fetchers,
249
- options,
250
- }) => {
251
- const disclosure =
252
- disclosureType === DisclosureType.NEW
253
- ? await fetchers.createDisclosure(newDisclosureRequest)
254
- : await getDisclosure(disclosures, fetchers, options);
255
- return disclosure;
278
+ const validateOptions = (options) => {
279
+ if (options.dryrun == null && options.endpoint == null) {
280
+ throw new Error(
281
+ '"-e" or "--endpoint" is required unless executing a "dryrun"'
282
+ );
283
+ }
284
+ if (options.endpoint != null && options.authToken == null) {
285
+ throw new Error('"-a" or "--auth-token" is required');
286
+ }
287
+
288
+ validateDirectoryExists(options);
289
+
290
+ validateCredentialType(options.idCredentialType);
291
+ };
292
+
293
+ const validateCredentialType = (idCredentialType) => {
294
+ const allowedIdCredentialTypes = values(CREDENTIAL_TYPES);
295
+ if (
296
+ !idCredentialType ||
297
+ !allowedIdCredentialTypes.includes(idCredentialType)
298
+ ) {
299
+ throw new Error(
300
+ `${idCredentialType} doesn't exist. Please use one of ${allowedIdCredentialTypes}`
301
+ );
302
+ }
256
303
  };
257
304
 
258
305
  module.exports = { runBatchIssuing };