@raisenow/tamaro-cli 3.1.0-dev.1 → 3.2.0-dev.1
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/README.md +6 -3
- package/dist/cli.js +113 -12
- package/package.json +15 -14
package/README.md
CHANGED
|
@@ -282,9 +282,12 @@ pnpx @raisenow/tamaro-cli update-epms --tag WEB-123
|
|
|
282
282
|
|
|
283
283
|
## `validate`
|
|
284
284
|
|
|
285
|
-
Validate the current configuration and fail on known issues such as
|
|
286
|
-
|
|
287
|
-
|
|
285
|
+
Validate the current configuration and fail on known issues such as:
|
|
286
|
+
|
|
287
|
+
- Invalid email templates
|
|
288
|
+
- Invalid `organisation_email` / `blind_copy` values in `email-config/parameters.yaml`
|
|
289
|
+
- Invalid `.env` values
|
|
290
|
+
- RaiseNow-generated tax receipts enabled without an uploaded organisation template
|
|
288
291
|
|
|
289
292
|
When `autogeneratedDonationReceiptEnabled` (or
|
|
290
293
|
`autogenerated_donation_receipt_enabled`) is `true`, this command authenticates
|
package/dist/cli.js
CHANGED
|
@@ -323,8 +323,7 @@ var EpmsClient = class EpmsClient {
|
|
|
323
323
|
}
|
|
324
324
|
async listOrganisationFiles(organisationUuid) {
|
|
325
325
|
try {
|
|
326
|
-
|
|
327
|
-
return Array.isArray(response) ? response : response.data ?? [];
|
|
326
|
+
return await this.#epmsClient.get(`organisations/${organisationUuid}/files`).json();
|
|
328
327
|
} catch (error) {
|
|
329
328
|
throw new Error(`Failed to list organisation files for organisation UUID: ${organisationUuid}`, { cause: error });
|
|
330
329
|
}
|
|
@@ -483,7 +482,7 @@ const donationReceiptTemplateExists = (files, accountUuid) => {
|
|
|
483
482
|
* Locate the line of a top-level `key:` assignment in the config.yml source,
|
|
484
483
|
* so the error can be rendered as a code frame pointing at the value.
|
|
485
484
|
*/
|
|
486
|
-
const findKeyLocation$
|
|
485
|
+
const findKeyLocation$2 = (source, key) => {
|
|
487
486
|
const lines = source.split("\n");
|
|
488
487
|
const assignment = new RegExp(String.raw`^\s*${key}\s*:`);
|
|
489
488
|
const index = lines.findIndex((line) => assignment.test(line));
|
|
@@ -510,8 +509,8 @@ const findKeyLocation$1 = (source, key) => {
|
|
|
510
509
|
* 51 | autogeneratedDonationReceiptEnabled: true
|
|
511
510
|
* | ^^^^ No donation tax receipt PDF template found…
|
|
512
511
|
*/
|
|
513
|
-
const formatError$
|
|
514
|
-
const location = findKeyLocation$
|
|
512
|
+
const formatError$2 = (configYmlPath, source, key, message) => {
|
|
513
|
+
const location = findKeyLocation$2(source, key);
|
|
515
514
|
if (!location) return `${configYmlPath}: ${key}: ${message}`;
|
|
516
515
|
const frame = codeFrameColumns(source, location, {
|
|
517
516
|
forceColor: true,
|
|
@@ -544,7 +543,7 @@ const validateDonationReceiptTemplates = async (configYmlPath) => {
|
|
|
544
543
|
if (!accountUuid) continue;
|
|
545
544
|
const epmsClient = epmsAuthenticate(env);
|
|
546
545
|
const organisationUuid = await epmsClient.getOrganisationIdByAccountUuid(accountUuid);
|
|
547
|
-
if (!donationReceiptTemplateExists(await epmsClient.listOrganisationFiles(organisationUuid), accountUuid)) errors.push(formatError$
|
|
546
|
+
if (!donationReceiptTemplateExists(await epmsClient.listOrganisationFiles(organisationUuid), accountUuid)) errors.push(formatError$2(configYmlPath, source, flagKey, `No donation tax receipt PDF template found for the ${label} account (${accountUuid}). See: ${DONATION_RECEIPT_VALIDATION_GUIDE_URL}`));
|
|
548
547
|
}
|
|
549
548
|
return errors;
|
|
550
549
|
};
|
|
@@ -685,7 +684,7 @@ const coreVersionExists = async (version) => {
|
|
|
685
684
|
* Locate the line of a `KEY=` assignment in the .env source, so the error can
|
|
686
685
|
* be rendered as a code frame pointing at the value.
|
|
687
686
|
*/
|
|
688
|
-
const findKeyLocation = (source, key) => {
|
|
687
|
+
const findKeyLocation$1 = (source, key) => {
|
|
689
688
|
const lines = source.split("\n");
|
|
690
689
|
const assignment = new RegExp(String.raw`^\s*${key}\s*=`);
|
|
691
690
|
const index = lines.findIndex((line) => assignment.test(line));
|
|
@@ -712,8 +711,8 @@ const findKeyLocation = (source, key) => {
|
|
|
712
711
|
* 2 | CORE_URL_PATTERN=adsf
|
|
713
712
|
* | ^^^^ Must contain the "{{version}}" placeholder
|
|
714
713
|
*/
|
|
715
|
-
const formatError = (dotEnvPath, source, key, message) => {
|
|
716
|
-
const location = findKeyLocation(source, key);
|
|
714
|
+
const formatError$1 = (dotEnvPath, source, key, message) => {
|
|
715
|
+
const location = findKeyLocation$1(source, key);
|
|
717
716
|
if (!location) return `${dotEnvPath}: ${key}: ${message}`;
|
|
718
717
|
const frame = codeFrameColumns(source, location, {
|
|
719
718
|
forceColor: true,
|
|
@@ -734,16 +733,117 @@ const validateEnv = async (dotEnvPath) => {
|
|
|
734
733
|
const errors = [];
|
|
735
734
|
for (const issue of result.error.issues) {
|
|
736
735
|
const keys = issue.code === "unrecognized_keys" ? issue.keys : [String(issue.path[0] ?? "")];
|
|
737
|
-
for (const key of keys) errors.push(formatError(dotEnvPath, source, key, issue.message));
|
|
736
|
+
for (const key of keys) errors.push(formatError$1(dotEnvPath, source, key, issue.message));
|
|
738
737
|
}
|
|
739
738
|
return errors;
|
|
740
739
|
}
|
|
741
740
|
if (parsed.CORE_VERSION) {
|
|
742
|
-
if (await coreVersionExists(parsed.CORE_VERSION) === false) return [formatError(dotEnvPath, source, "CORE_VERSION", `Version "${parsed.CORE_VERSION}" does not exist. See all versions at https://www.npmjs.com/package/@raisenow/tamaro-core?activeTab=versions`)];
|
|
741
|
+
if (await coreVersionExists(parsed.CORE_VERSION) === false) return [formatError$1(dotEnvPath, source, "CORE_VERSION", `Version "${parsed.CORE_VERSION}" does not exist. See all versions at https://www.npmjs.com/package/@raisenow/tamaro-core?activeTab=versions`)];
|
|
743
742
|
}
|
|
744
743
|
return [];
|
|
745
744
|
};
|
|
746
745
|
//#endregion
|
|
746
|
+
//#region src/lib/validators/validateParametersYaml.ts
|
|
747
|
+
/** Email-valued keys checked in `email-config/parameters.yaml` when set. */
|
|
748
|
+
const EMAIL_KEYS = ["organisation_email", "blind_copy"];
|
|
749
|
+
const emailSchema = z.email();
|
|
750
|
+
/**
|
|
751
|
+
* Bare `key:`, YAML null, or blank strings are common in customer configs and
|
|
752
|
+
* must not fail validation.
|
|
753
|
+
*/
|
|
754
|
+
const isUnsetEmailValue = (value) => value == void 0 || typeof value === "string" && value.trim() === "";
|
|
755
|
+
/**
|
|
756
|
+
* Locate a `key: value` assignment in the YAML source so the error can be
|
|
757
|
+
* rendered as a code frame pointing at the value.
|
|
758
|
+
*
|
|
759
|
+
* Prefers a key+value match so repeated keys across locales (`all` / `de` /
|
|
760
|
+
* `en`, …) point at the right line; falls back to the first key match.
|
|
761
|
+
*/
|
|
762
|
+
const findKeyLocation = (source, key, value) => {
|
|
763
|
+
const lines = source.split("\n");
|
|
764
|
+
let index = -1;
|
|
765
|
+
if (value !== void 0) {
|
|
766
|
+
const escapedValue = value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
767
|
+
const assignment = new RegExp(String.raw`^\s*${key}\s*:\s*['"]?${escapedValue}['"]?\s*(?:#.*)?$`);
|
|
768
|
+
index = lines.findIndex((line) => assignment.test(line));
|
|
769
|
+
}
|
|
770
|
+
if (index === -1) {
|
|
771
|
+
const keyOnly = new RegExp(String.raw`^\s*${key}\s*:`);
|
|
772
|
+
index = lines.findIndex((line) => keyOnly.test(line));
|
|
773
|
+
}
|
|
774
|
+
if (index === -1) return;
|
|
775
|
+
const line = index + 1;
|
|
776
|
+
const valueStart = lines[index].indexOf(":") + 1;
|
|
777
|
+
return {
|
|
778
|
+
end: {
|
|
779
|
+
column: lines[index].length,
|
|
780
|
+
line
|
|
781
|
+
},
|
|
782
|
+
start: {
|
|
783
|
+
column: valueStart + 1,
|
|
784
|
+
line
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
};
|
|
788
|
+
/**
|
|
789
|
+
* Format an error for a `key:` assignment as a code frame pointing at the
|
|
790
|
+
* value, so it looks fancy like this:
|
|
791
|
+
*
|
|
792
|
+
* email-config/parameters.yaml:3
|
|
793
|
+
* 2 | organisation_name: Example
|
|
794
|
+
* 3 | organisation_email: not-an-email
|
|
795
|
+
* | ^^^^^^^^^^^^ Invalid email address
|
|
796
|
+
*/
|
|
797
|
+
const formatError = (parametersPath, source, key, message, value) => {
|
|
798
|
+
const location = findKeyLocation(source, key, value);
|
|
799
|
+
if (!location) return `${parametersPath}: ${key}: ${message}`;
|
|
800
|
+
const frame = codeFrameColumns(source, location, {
|
|
801
|
+
forceColor: true,
|
|
802
|
+
highlightCode: true,
|
|
803
|
+
message
|
|
804
|
+
});
|
|
805
|
+
return `\n${parametersPath}:${location.start.line}\n${frame}`;
|
|
806
|
+
};
|
|
807
|
+
/**
|
|
808
|
+
* Validate `organisation_email` and `blind_copy` under each locale block
|
|
809
|
+
* (`all`, `de`, `en`, …) in `email-config/parameters.yaml`.
|
|
810
|
+
*
|
|
811
|
+
* Unset values are allowed; non-empty values must be valid email addresses.
|
|
812
|
+
*/
|
|
813
|
+
const validateParametersYaml = (parametersPath) => {
|
|
814
|
+
if (!fs.existsSync(parametersPath)) return [];
|
|
815
|
+
const source = fs.readFileSync(parametersPath, "utf8");
|
|
816
|
+
let parsed;
|
|
817
|
+
try {
|
|
818
|
+
parsed = yaml.load(source);
|
|
819
|
+
} catch (error) {
|
|
820
|
+
return [`${parametersPath}: Invalid YAML: ${error instanceof Error ? error.message : String(error)}`];
|
|
821
|
+
}
|
|
822
|
+
if (parsed == void 0) return [];
|
|
823
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) return [`${parametersPath}: Expected a mapping of locale blocks (e.g. \`all\`), got ${Array.isArray(parsed) ? "an array" : typeof parsed}`];
|
|
824
|
+
const errors = [];
|
|
825
|
+
for (const [locale, block] of Object.entries(parsed)) {
|
|
826
|
+
if (block == void 0) continue;
|
|
827
|
+
if (typeof block !== "object" || Array.isArray(block)) {
|
|
828
|
+
errors.push(`${parametersPath}: ${locale}: Expected a mapping of parameters, got ${Array.isArray(block) ? "an array" : typeof block}`);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
const parameters = block;
|
|
832
|
+
for (const key of EMAIL_KEYS) {
|
|
833
|
+
if (!(key in parameters)) continue;
|
|
834
|
+
const value = parameters[key];
|
|
835
|
+
if (isUnsetEmailValue(value)) continue;
|
|
836
|
+
if (typeof value !== "string") {
|
|
837
|
+
errors.push(formatError(parametersPath, source, key, "Must be a string email address"));
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
const result = emailSchema.safeParse(value);
|
|
841
|
+
if (!result.success) errors.push(formatError(parametersPath, source, key, result.error.issues[0]?.message ?? "Invalid email address", value));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return errors;
|
|
845
|
+
};
|
|
846
|
+
//#endregion
|
|
747
847
|
//#region src/commands/validate.ts
|
|
748
848
|
/**
|
|
749
849
|
* Validates the current Tamaro configuration to avoid common errors.
|
|
@@ -756,6 +856,7 @@ const validate = async () => {
|
|
|
756
856
|
const errors = [
|
|
757
857
|
...await validateEnv(".env"),
|
|
758
858
|
...validateEmailTemplates(globSync("email-config/templates/**/*.hbs").toSorted()),
|
|
859
|
+
...validateParametersYaml("email-config/parameters.yaml"),
|
|
759
860
|
...await validateDonationReceiptTemplates("config.yml")
|
|
760
861
|
];
|
|
761
862
|
if (errors.length > 0) {
|
|
@@ -1434,7 +1535,7 @@ const promptBucketTamaroEmailConfig = async () => {
|
|
|
1434
1535
|
//#endregion
|
|
1435
1536
|
//#region package.json
|
|
1436
1537
|
var name = "@raisenow/tamaro-cli";
|
|
1437
|
-
var version = "3.
|
|
1538
|
+
var version = "3.2.0-dev.1";
|
|
1438
1539
|
//#endregion
|
|
1439
1540
|
//#region src/cli.ts
|
|
1440
1541
|
const cli = createCommand().name(name).description("CLI for Tamaro Customer Configurations development").version(version, "-v, --version");
|
package/package.json
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raisenow/tamaro-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0-dev.1",
|
|
4
4
|
"author": {
|
|
5
5
|
"name": "RaiseNow",
|
|
6
6
|
"email": "development@raisenow.com"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
|
+
"bin": "dist/cli.js",
|
|
10
|
+
"packageManager": "pnpm@11.10.0",
|
|
9
11
|
"engines": {
|
|
10
12
|
"node": ">=24.18.0",
|
|
11
13
|
"pnpm": ">=11.10.0",
|
|
12
14
|
"npm": "please-use-pnpm",
|
|
13
15
|
"yarn": "please-use-pnpm"
|
|
14
16
|
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"typecheck": "tsc --build --emitDeclarationOnly",
|
|
19
|
+
"build": "tsdown",
|
|
20
|
+
"dev": "pnpm build --watch",
|
|
21
|
+
"readme": "pnpx http-server . --cors -o /readme.html",
|
|
22
|
+
"lint": "DEBUG=eslint:eslint-helpers eslint .",
|
|
23
|
+
"format": "prettier . --write --ignore-path ./.prettierignore",
|
|
24
|
+
"test": "vitest",
|
|
25
|
+
"prepare": "husky",
|
|
26
|
+
"prepublishOnly": "pnpm build"
|
|
27
|
+
},
|
|
15
28
|
"dependencies": {
|
|
16
29
|
"@aws-sdk/client-s3": "^3.1081.0",
|
|
17
30
|
"@babel/code-frame": "^8.0.0",
|
|
@@ -54,17 +67,5 @@
|
|
|
54
67
|
"eslint --fix",
|
|
55
68
|
"prettier --write"
|
|
56
69
|
]
|
|
57
|
-
},
|
|
58
|
-
"scripts": {
|
|
59
|
-
"typecheck": "tsc --build --emitDeclarationOnly",
|
|
60
|
-
"build": "tsdown",
|
|
61
|
-
"dev": "pnpm build --watch",
|
|
62
|
-
"readme": "pnpx http-server . --cors -o /readme.html",
|
|
63
|
-
"lint": "DEBUG=eslint:eslint-helpers eslint .",
|
|
64
|
-
"format": "prettier . --write --ignore-path ./.prettierignore",
|
|
65
|
-
"test": "vitest"
|
|
66
|
-
},
|
|
67
|
-
"bin": {
|
|
68
|
-
"tamaro-cli": "dist/cli.js"
|
|
69
70
|
}
|
|
70
|
-
}
|
|
71
|
+
}
|