@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 +3 -2
- package/src/batch-issuing/constants.js +25 -0
- package/src/batch-issuing/file-writers.js +2 -5
- package/src/batch-issuing/index.js +50 -48
- package/src/batch-issuing/orchestrators.js +146 -99
- package/src/batch-issuing/prepare-data.js +40 -81
- package/src/batch-issuing/validate-directory-exists.js +3 -3
- package/src/helpers/index.js +2 -0
- package/src/helpers/load-csv.js +14 -11
- package/src/helpers/parse-column.js +8 -0
- package/src/helpers/prepare-variable-sets.js +23 -0
- package/src/vendor-credentials/README.md +48 -8
- package/src/vendor-credentials/index.js +10 -14
- package/src/vendor-credentials/orchestrator.js +22 -0
- package/src/vendor-credentials/prepare-data.js +5 -9
- package/test/batch-issuing.test.js +448 -75
- package/test/data/batch-vars-offerids.csv +3 -3
- package/test/data/email-offer.template.json +1 -1
- package/test/vendor-credentials.test.js +6 -4
@@ -1,60 +1,21 @@
|
|
1
|
-
const { get, includes, isNil, map, omitBy,
|
1
|
+
const { get, includes, isNil, map, omitBy, isEmpty } = require('lodash/fp');
|
2
2
|
const { formatISO } = require('date-fns/fp');
|
3
3
|
const { nanoid } = require('nanoid');
|
4
|
-
const {
|
5
|
-
|
6
|
-
|
7
|
-
computeActivationDate,
|
8
|
-
} = require('../helpers');
|
9
|
-
const { DisclosureType } = require('./constants');
|
4
|
+
const { loadHandlebarsTemplate, computeActivationDate } = require('../helpers');
|
5
|
+
const { idCredentialTypeToPick } = require('./constants');
|
6
|
+
const { getColIndex, prepareVariableSets } = require('../helpers');
|
10
7
|
|
11
8
|
const EMAIL_REGEX =
|
12
9
|
// eslint-disable-next-line max-len
|
13
10
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
14
|
-
|
15
11
|
const PHONE_REGEX = /\(?([0-9]{3})\)?([ .-]?)([0-9]{3})\2([0-9]{4})/;
|
16
|
-
const VENDOR_USER_ID_PATH = 'credentialSubject.vendorUserId';
|
17
|
-
|
18
|
-
const CREDENTIAL_TYPES = {
|
19
|
-
EMAIL: 'EmailV1.0',
|
20
|
-
PHONE: 'PhoneV1.0',
|
21
|
-
DRIVER_LICENSE: 'DriversLicenseV1.0',
|
22
|
-
ID_DOCUMENT: 'IdDocumentV1.0',
|
23
|
-
NATIONAL_ID_CARD: 'NationalIdCardV1.0',
|
24
|
-
PASSPORT: 'PassportV1.0',
|
25
|
-
PROOF_OF_AGE: 'ProofOfAgeV1.0',
|
26
|
-
RESIDENT_PERMIT: 'ResidentPermitV1.0',
|
27
|
-
};
|
28
|
-
|
29
|
-
const DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH =
|
30
|
-
'$.idDocumentCredentials[*].credentialSubject.identifier';
|
31
|
-
const credentialTypeToPick = {
|
32
|
-
[CREDENTIAL_TYPES.EMAIL]: '$.emails',
|
33
|
-
[CREDENTIAL_TYPES.PHONE]: '$.phones',
|
34
|
-
[CREDENTIAL_TYPES.DRIVER_LICENSE]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
35
|
-
[CREDENTIAL_TYPES.ID_DOCUMENT]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
36
|
-
[CREDENTIAL_TYPES.NATIONAL_ID_CARD]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
37
|
-
[CREDENTIAL_TYPES.PASSPORT]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
38
|
-
[CREDENTIAL_TYPES.PROOF_OF_AGE]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
39
|
-
[CREDENTIAL_TYPES.RESIDENT_PERMIT]: DEFAULT_ID_DOCUMENT_CREDENTIALS_PATH,
|
40
|
-
};
|
41
12
|
|
42
|
-
const
|
43
|
-
disclosureType,
|
44
|
-
{ csvFilename, offerTemplateFilename, vars: defaultVars, did, ...options }
|
45
|
-
) => {
|
46
|
-
validateCredentialType(options.credentialType);
|
47
|
-
const newDisclosureRequest = prepareNewDisclosureRequest(
|
48
|
-
disclosureType,
|
49
|
-
options
|
50
|
-
);
|
13
|
+
const VENDOR_USER_ID_PATH = 'credentialSubject.vendorUserId';
|
51
14
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
const offerTemplate = loadHandlebarsTemplate(offerTemplateFilename);
|
57
|
-
const newExchangeOffers = map(
|
15
|
+
const prepareExchangeOffers = async (csvHeaders, csvRows, options) => {
|
16
|
+
const variableSets = await prepareVariableSets(csvHeaders, csvRows, options);
|
17
|
+
const offerTemplate = loadHandlebarsTemplate(options.offerTemplateFilename);
|
18
|
+
return map(
|
58
19
|
(variableSet) => ({
|
59
20
|
newOffer: prepareNewOffer({
|
60
21
|
template: offerTemplate,
|
@@ -65,15 +26,13 @@ const prepareData = async (
|
|
65
26
|
}),
|
66
27
|
variableSets
|
67
28
|
);
|
68
|
-
|
69
|
-
return { newDisclosureRequest, newExchangeOffers };
|
70
29
|
};
|
71
30
|
|
72
31
|
const prepareNewOffer = ({ template, variableSet, label }) => {
|
73
32
|
validateVariableSet(variableSet);
|
74
33
|
const offerString = template(variableSet);
|
75
34
|
const offer = JSON.parse(offerString);
|
76
|
-
|
35
|
+
validateVendorUserIdInOffer(offer, variableSet);
|
77
36
|
return omitBy(isNil, {
|
78
37
|
...offer,
|
79
38
|
offerId: variableSet.offerId ?? nanoid(),
|
@@ -81,15 +40,6 @@ const prepareNewOffer = ({ template, variableSet, label }) => {
|
|
81
40
|
});
|
82
41
|
};
|
83
42
|
|
84
|
-
const validateCredentialType = (credentialType) => {
|
85
|
-
const allowedCredentialTypes = values(CREDENTIAL_TYPES);
|
86
|
-
if (!credentialType || !allowedCredentialTypes.includes(credentialType)) {
|
87
|
-
throw new Error(
|
88
|
-
`${credentialType} doesn't exist. Please use one of ${allowedCredentialTypes}`
|
89
|
-
);
|
90
|
-
}
|
91
|
-
};
|
92
|
-
|
93
43
|
const validateVariableSet = (variableSet) => {
|
94
44
|
if (
|
95
45
|
variableSet.email == null &&
|
@@ -99,13 +49,22 @@ const validateVariableSet = (variableSet) => {
|
|
99
49
|
throw new Error('"email", "phone", or "identifier" column must be defined');
|
100
50
|
}
|
101
51
|
|
102
|
-
const { email, phone } = variableSet;
|
52
|
+
const { email, phone, vendorUserId } = variableSet;
|
103
53
|
|
104
|
-
|
105
|
-
|
54
|
+
validateVendorUserIdVariable(vendorUserId);
|
55
|
+
validateEmailVariable(email);
|
56
|
+
validatePhoneVariable(phone);
|
106
57
|
};
|
107
58
|
|
108
|
-
const
|
59
|
+
const validateVendorUserIdVariable = (vendorUserId) => {
|
60
|
+
if (isEmpty(vendorUserId)) {
|
61
|
+
throw new Error(
|
62
|
+
'vendorUserId variable must exist. Use the -u <column> option'
|
63
|
+
);
|
64
|
+
}
|
65
|
+
};
|
66
|
+
|
67
|
+
const validateEmailVariable = (email) => {
|
109
68
|
if (!email) return;
|
110
69
|
|
111
70
|
if (!EMAIL_REGEX.test(email)) {
|
@@ -113,7 +72,7 @@ const validateEmail = (email) => {
|
|
113
72
|
}
|
114
73
|
};
|
115
74
|
|
116
|
-
const
|
75
|
+
const validatePhoneVariable = (phone) => {
|
117
76
|
if (!phone) return;
|
118
77
|
|
119
78
|
if (!PHONE_REGEX.test(phone)) {
|
@@ -121,7 +80,7 @@ const validatePhone = (phone) => {
|
|
121
80
|
}
|
122
81
|
};
|
123
82
|
|
124
|
-
const
|
83
|
+
const validateVendorUserIdInOffer = (offer, variableSet) => {
|
125
84
|
const value = get(VENDOR_USER_ID_PATH, offer);
|
126
85
|
if (!includes(value, variableSet)) {
|
127
86
|
throw new Error(
|
@@ -132,7 +91,7 @@ const validateVendorUserId = (offer, variableSet) => {
|
|
132
91
|
}
|
133
92
|
};
|
134
93
|
|
135
|
-
const
|
94
|
+
const matcherToIdCredentialType = (idCredentialType, variableSet) => {
|
136
95
|
const typeToVariable = {
|
137
96
|
'EmailV1.0': variableSet.email,
|
138
97
|
'PhoneV1.0': variableSet.phone,
|
@@ -143,32 +102,31 @@ const matcherToCredentialType = (credentialType, variableSet) => {
|
|
143
102
|
'ProofOfAgeV1.0': variableSet.identifier,
|
144
103
|
'ResidentPermitV1.0': variableSet.identifier,
|
145
104
|
};
|
146
|
-
return typeToVariable[
|
105
|
+
return typeToVariable[idCredentialType] || variableSet.email;
|
147
106
|
};
|
148
107
|
|
149
|
-
const prepareNewExchange = ({ variableSet, label,
|
108
|
+
const prepareNewExchange = ({ variableSet, label, idCredentialType }) =>
|
150
109
|
omitBy(isNil, {
|
151
110
|
type: 'ISSUING',
|
152
111
|
identityMatcherValues: [
|
153
|
-
|
112
|
+
matcherToIdCredentialType(idCredentialType, variableSet),
|
154
113
|
],
|
155
114
|
label,
|
156
115
|
});
|
157
116
|
|
158
117
|
const prepareNewDisclosureRequest = (
|
159
|
-
|
118
|
+
csvHeaders,
|
160
119
|
{
|
161
120
|
label,
|
162
121
|
termsUrl,
|
163
122
|
purpose,
|
164
|
-
|
165
|
-
|
123
|
+
idCredentialType,
|
124
|
+
identifierMatchColumn,
|
125
|
+
vendorUseridColumn,
|
126
|
+
authTokenExpiresIn,
|
166
127
|
...options
|
167
128
|
}
|
168
129
|
) => {
|
169
|
-
if (disclosureType !== DisclosureType.NEW) {
|
170
|
-
return undefined;
|
171
|
-
}
|
172
130
|
const activationDate = computeActivationDate(options);
|
173
131
|
|
174
132
|
const newDisclosureRequest = {
|
@@ -177,18 +135,18 @@ const prepareNewDisclosureRequest = (
|
|
177
135
|
vendorEndpoint: 'integrated-issuing-identification',
|
178
136
|
types: [
|
179
137
|
{
|
180
|
-
type:
|
138
|
+
type: idCredentialType,
|
181
139
|
},
|
182
140
|
],
|
183
141
|
identityMatchers: {
|
184
142
|
rules: [
|
185
143
|
{
|
186
|
-
valueIndex:
|
187
|
-
path: [
|
144
|
+
valueIndex: getColIndex(csvHeaders, identifierMatchColumn),
|
145
|
+
path: [idCredentialTypeToPick[idCredentialType]],
|
188
146
|
rule: 'pick',
|
189
147
|
},
|
190
148
|
],
|
191
|
-
vendorUserIdIndex:
|
149
|
+
vendorUserIdIndex: getColIndex(csvHeaders, vendorUseridColumn),
|
192
150
|
},
|
193
151
|
setIssuingDefault: true,
|
194
152
|
duration: '1h', // 1 hour by default
|
@@ -204,5 +162,6 @@ const prepareNewDisclosureRequest = (
|
|
204
162
|
};
|
205
163
|
|
206
164
|
module.exports = {
|
207
|
-
|
165
|
+
prepareNewDisclosureRequest,
|
166
|
+
prepareExchangeOffers,
|
208
167
|
};
|
@@ -1,8 +1,8 @@
|
|
1
1
|
const { existsSync } = require('fs');
|
2
2
|
|
3
|
-
const validateDirectoryExists = (
|
4
|
-
if (!existsSync(path)) {
|
5
|
-
throw new Error(
|
3
|
+
const validateDirectoryExists = (options) => {
|
4
|
+
if (options.dryrun == null && !existsSync(options.path)) {
|
5
|
+
throw new Error('Path does not exist. Check the -p var');
|
6
6
|
}
|
7
7
|
};
|
8
8
|
|
package/src/helpers/index.js
CHANGED
package/src/helpers/load-csv.js
CHANGED
@@ -1,29 +1,32 @@
|
|
1
1
|
const fs = require('fs');
|
2
2
|
const csv = require('csv-parser');
|
3
|
-
const {
|
3
|
+
const { isString, indexOf } = require('lodash/fp');
|
4
4
|
|
5
5
|
const loadCsv = (fileName) => {
|
6
6
|
return new Promise((resolve, reject) => {
|
7
|
-
const
|
7
|
+
const csvRows = [];
|
8
|
+
let csvHeaders;
|
8
9
|
fs.createReadStream(fileName)
|
9
10
|
.pipe(csv())
|
11
|
+
.on('headers', (headers) => {
|
12
|
+
// eslint-disable-next-line better-mutation/no-mutation
|
13
|
+
csvHeaders = headers;
|
14
|
+
})
|
10
15
|
.on('data', (data) => {
|
11
16
|
// eslint-disable-next-line better-mutation/no-mutating-methods
|
12
|
-
|
17
|
+
csvRows.push(data);
|
13
18
|
})
|
14
19
|
.on('err', (err) => reject(err))
|
15
20
|
.on('end', () => {
|
16
|
-
resolve(
|
21
|
+
resolve([csvHeaders, csvRows]);
|
17
22
|
});
|
18
23
|
});
|
19
24
|
};
|
20
25
|
|
21
|
-
const
|
22
|
-
|
26
|
+
const getColIndex = (csvHeaders, column) =>
|
27
|
+
isString(column) ? indexOf(column, csvHeaders) : column;
|
23
28
|
|
24
|
-
const
|
25
|
-
|
26
|
-
return mergeRowsWithDefaults(defaultVars)(csvRows);
|
27
|
-
};
|
29
|
+
const getColName = (csvHeaders, column) =>
|
30
|
+
isString(column) ? column : csvHeaders[column];
|
28
31
|
|
29
|
-
module.exports = { loadCsv,
|
32
|
+
module.exports = { loadCsv, getColIndex, getColName };
|
@@ -0,0 +1,23 @@
|
|
1
|
+
const { map } = require('lodash/fp');
|
2
|
+
const { getColName } = require('./load-csv');
|
3
|
+
|
4
|
+
const prepareVariableSets = async (
|
5
|
+
csvHeaders,
|
6
|
+
csvRows,
|
7
|
+
{ vendorUseridColumn, vars, did }
|
8
|
+
) => {
|
9
|
+
const overrideVars = { ...vars };
|
10
|
+
if (did != null) {
|
11
|
+
overrideVars.did = did;
|
12
|
+
}
|
13
|
+
return map(
|
14
|
+
(csvRow) => ({
|
15
|
+
...csvRow,
|
16
|
+
...overrideVars,
|
17
|
+
vendorUserId: csvRow[getColName(csvHeaders, vendorUseridColumn)],
|
18
|
+
}),
|
19
|
+
csvRows
|
20
|
+
);
|
21
|
+
};
|
22
|
+
|
23
|
+
module.exports = { prepareVariableSets };
|
@@ -11,22 +11,62 @@ Checkout the [test data](../test/data).
|
|
11
11
|
|
12
12
|
## How to Use
|
13
13
|
`data-loader vendorcreds [options]`
|
14
|
-
### Options
|
15
14
|
|
16
|
-
|
15
|
+
### Config
|
17
16
|
|
18
|
-
|
17
|
+
#### General config
|
19
18
|
|
20
|
-
`-
|
21
|
-
|
19
|
+
`-c [CSV_FILENAME]` **required** parameter containing the csv file
|
20
|
+
|
21
|
+
`-p [OUTPUT_PATH]` **required** the output directory to use where QR codes and output state files are stored
|
22
|
+
|
23
|
+
`-l [LABEL]` A label to attach to the records added to the agent
|
24
|
+
|
25
|
+
`--dry-run` Run a test that parses the CSV and creates offers, but does not attempt to write the data to an agent. great for testing!
|
26
|
+
|
27
|
+
#### Issuing config
|
28
|
+
|
29
|
+
`--new` to create a new issuing configuration ("disclosure")
|
30
|
+
|
31
|
+
`-i [DISCLOSURE_ID]` an existing disclosure to use for the batch issuing
|
32
|
+
|
33
|
+
`-d [DID]` **required** the issuer's DID
|
34
|
+
|
35
|
+
`-t [URL]` **required** the url to the T&Cs that holder must consent to
|
36
|
+
|
37
|
+
`--purpose` The purpose to display to the user. Use a maximum for 64 chars. Default is "Career Credential Issuing"
|
38
|
+
|
39
|
+
`--authTokenExpiresIn` The number of minutes that the offer will be available for after activation. Default is 365 days.
|
40
|
+
|
41
|
+
#### User authentication and matching config
|
42
|
+
|
43
|
+
`-y [ID_CREDENTIAL_TYPE]` the credential type used for identifying the user. Default is Email.
|
44
|
+
|
45
|
+
`-u [COLUMN]` the column from the CSV for the user id. Value is made available as "vendorUserId" in the offer template. Default is the first column
|
46
|
+
|
47
|
+
`-m [COLUMN]` the column from the CSV for the user to be matched against the ID credential's "identifier" property. Default is the first column
|
48
|
+
|
49
|
+
#### Offer generation config
|
50
|
+
|
51
|
+
`-o [OFFER_TEMPLATE_FILENAME]` **required** offer handlebars template. Use moustaches around variables such as `{{did}}`
|
22
52
|
|
23
53
|
`--var=[VAR_NAME]=[VAR_VALUE]` variables used in the templates can be specified on the command line. They override any csv values
|
24
54
|
|
25
|
-
|
55
|
+
#### Credential Agent config
|
56
|
+
|
57
|
+
`-e [URL]` **required if not a dryrun** the endpoint of the mockvendor server
|
58
|
+
|
59
|
+
`-t [AUTH_TOKEN]` **required if not a dryrun** the bearer token to use when calling the mockvendor server
|
60
|
+
|
61
|
+
`--legacy` the target credential agent is running in the "LEGACY" offer type mode. Default is false
|
62
|
+
|
63
|
+
#### Output config
|
64
|
+
|
65
|
+
`-x` if passed an output csv is generated including the vendor's user id as the first column and the generated qrcode filename and deeplink
|
26
66
|
|
27
|
-
|
67
|
+
`--x-name [OUTPUT_CSV_NAME]` The file name for the output CSV. Default is "output"
|
28
68
|
|
29
69
|
### Dry Run Example
|
30
70
|
Dry runs that print out what updates will be issued should omit the `-e` prop
|
31
71
|
|
32
|
-
`./data-loader vendorcreds -c ./test/data/variables.csv -o ./test/data/offer.template.json -
|
72
|
+
`./data-loader vendorcreds -c ./test/data/variables.csv -o ./test/data/offer.template.json -d did:ion:sap456`
|
@@ -1,10 +1,7 @@
|
|
1
|
-
/* eslint-disable no-await-in-loop */
|
2
|
-
|
3
1
|
const { program } = require('commander');
|
4
2
|
const { reduce } = require('lodash/fp');
|
5
|
-
const {
|
6
|
-
const {
|
7
|
-
const { initExecuteUpdate } = require('./execute-update');
|
3
|
+
const { printInfo, parseColumn } = require('../helpers');
|
4
|
+
const { executeVendorCredentials } = require('./orchestrator');
|
8
5
|
|
9
6
|
const parseVar = reduce((acc, pair) => {
|
10
7
|
const [key, value] = pair.split('=');
|
@@ -32,6 +29,13 @@ program
|
|
32
29
|
'-e, --endpoint <url>',
|
33
30
|
'Endpoint to call to upload the people and credentials to'
|
34
31
|
)
|
32
|
+
.option(
|
33
|
+
'-u --vendor-userid-column <vendorUseridColumn>',
|
34
|
+
`the column from the CSV that is users id. Value is made available as "vendorUserId" in the offer template. Accepts
|
35
|
+
header name or index. Default is 0.`,
|
36
|
+
parseColumn,
|
37
|
+
'0'
|
38
|
+
)
|
35
39
|
.option('-t, --auth-token <url>', 'Bearer Auth Token to use')
|
36
40
|
.option('-l, --label <label>', 'A label to attach to the documents inserted')
|
37
41
|
.option('-v, --var <var...>', 'A variable to add. use name=value')
|
@@ -40,14 +44,6 @@ program
|
|
40
44
|
// eslint-disable-next-line better-mutation/no-mutation
|
41
45
|
options.vars = parseVar(options.var);
|
42
46
|
printInfo(options);
|
43
|
-
|
44
|
-
const updates = await prepareData(options);
|
45
|
-
if (options.endpoint != null) {
|
46
|
-
for (const update of updates) {
|
47
|
-
await executeUpdate(update);
|
48
|
-
}
|
49
|
-
} else {
|
50
|
-
printInfo(JSON.stringify({ updates }));
|
51
|
-
}
|
47
|
+
await executeVendorCredentials(options);
|
52
48
|
})
|
53
49
|
.parseAsync(process.argv);
|
@@ -0,0 +1,22 @@
|
|
1
|
+
const { initExecuteUpdate } = require('./execute-update');
|
2
|
+
const { prepareData } = require('./prepare-data');
|
3
|
+
const { loadCsv, printInfo } = require('../helpers');
|
4
|
+
|
5
|
+
// eslint-disable-next-line consistent-return
|
6
|
+
const executeVendorCredentials = async (options) => {
|
7
|
+
const [csvHeaders, csvRows] = await loadCsv(options.csvFilename);
|
8
|
+
const updates = await prepareData(csvHeaders, csvRows, options);
|
9
|
+
|
10
|
+
if (options.endpoint == null) {
|
11
|
+
printInfo(JSON.stringify({ updates }));
|
12
|
+
return updates;
|
13
|
+
}
|
14
|
+
|
15
|
+
const executeUpdate = initExecuteUpdate(options);
|
16
|
+
for (const update of updates) {
|
17
|
+
// eslint-disable-next-line no-await-in-loop
|
18
|
+
await executeUpdate(update);
|
19
|
+
}
|
20
|
+
};
|
21
|
+
|
22
|
+
module.exports = { executeVendorCredentials };
|
@@ -2,16 +2,12 @@ const { map } = require('lodash/fp');
|
|
2
2
|
const {
|
3
3
|
loadHandlebarsTemplate,
|
4
4
|
} = require('../helpers/load-handlebars-template');
|
5
|
-
const {
|
5
|
+
const { prepareVariableSets } = require('../helpers');
|
6
6
|
|
7
|
-
const prepareData = async ({
|
8
|
-
|
9
|
-
|
10
|
-
personTemplateFilename,
|
11
|
-
label,
|
12
|
-
vars: defaultVars,
|
13
|
-
}) => {
|
14
|
-
const variableSets = await loadCsvVariableSets(csvFilename, defaultVars);
|
7
|
+
const prepareData = async (csvHeaders, csvRows, options) => {
|
8
|
+
const variableSets = await prepareVariableSets(csvHeaders, csvRows, options);
|
9
|
+
|
10
|
+
const { offerTemplateFilename, personTemplateFilename, label } = options;
|
15
11
|
const offerTemplate = loadHandlebarsTemplate(offerTemplateFilename);
|
16
12
|
const personTemplate = personTemplateFilename
|
17
13
|
? loadHandlebarsTemplate(personTemplateFilename)
|