@quotivity/cpq-inventory-core 0.1.0

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 (45) hide show
  1. package/assets/report/assets/index-CKPi--yh.css +1 -0
  2. package/assets/report/assets/index-CqUjsqpS.js +9 -0
  3. package/assets/report/fonts/LexendDeca-VariableFont_wght.ttf +0 -0
  4. package/assets/report/index.html +14 -0
  5. package/lib/banner.d.ts +7 -0
  6. package/lib/banner.js +13 -0
  7. package/lib/classify/advancedCondition.d.ts +34 -0
  8. package/lib/classify/advancedCondition.js +112 -0
  9. package/lib/classify/formula.d.ts +29 -0
  10. package/lib/classify/formula.js +125 -0
  11. package/lib/classify/index.d.ts +80 -0
  12. package/lib/classify/index.js +662 -0
  13. package/lib/classify/rules.d.ts +31 -0
  14. package/lib/classify/rules.js +868 -0
  15. package/lib/config/outbound.d.ts +31 -0
  16. package/lib/config/outbound.js +35 -0
  17. package/lib/connection.d.ts +42 -0
  18. package/lib/connection.js +52 -0
  19. package/lib/extract/index.d.ts +112 -0
  20. package/lib/extract/index.js +373 -0
  21. package/lib/extract/records.d.ts +421 -0
  22. package/lib/extract/records.js +327 -0
  23. package/lib/index.d.ts +30 -0
  24. package/lib/index.js +51 -0
  25. package/lib/liveness.d.ts +19 -0
  26. package/lib/liveness.js +34 -0
  27. package/lib/mapping.d.ts +14 -0
  28. package/lib/mapping.js +272 -0
  29. package/lib/report/assemble.d.ts +16 -0
  30. package/lib/report/assemble.js +499 -0
  31. package/lib/report/printDoc.d.ts +13 -0
  32. package/lib/report/printDoc.js +82 -0
  33. package/lib/report/shareSummary.d.ts +8 -0
  34. package/lib/report/shareSummary.js +54 -0
  35. package/lib/report/types.d.ts +146 -0
  36. package/lib/report/types.js +2 -0
  37. package/lib/server.d.ts +32 -0
  38. package/lib/server.js +185 -0
  39. package/lib/soql.d.ts +50 -0
  40. package/lib/soql.js +76 -0
  41. package/lib/summary.d.ts +14 -0
  42. package/lib/summary.js +143 -0
  43. package/lib/types.d.ts +22 -0
  44. package/lib/types.js +85 -0
  45. package/package.json +35 -0
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="referrer" content="no-referrer" />
7
+ <title>Quotivity CPQ Inventory</title>
8
+ <script type="module" crossorigin src="./assets/index-CqUjsqpS.js"></script>
9
+ <link rel="stylesheet" crossorigin href="./assets/index-CKPi--yh.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1,7 @@
1
+ /** Terminal copy shared by the plugin and the standalone script. */
2
+ export declare function launchBanner(org: {
3
+ name: string;
4
+ username: string;
5
+ }): string;
6
+ export declare function readyLine(url: string): string;
7
+ //# sourceMappingURL=banner.d.ts.map
package/lib/banner.js ADDED
@@ -0,0 +1,13 @@
1
+ /** Terminal copy shared by the plugin and the standalone script. */
2
+ export function launchBanner(org) {
3
+ return [
4
+ 'Quotivity CPQ Inventory',
5
+ `Connected to: ${org.name} (${org.username})`,
6
+ '',
7
+ 'This runs entirely on your machine. Opening your browser...',
8
+ ].join('\n');
9
+ }
10
+ export function readyLine(url) {
11
+ return `Report served at ${url} — bound to 127.0.0.1 only. Press Ctrl-C to stop.`;
12
+ }
13
+ //# sourceMappingURL=banner.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Parser for `SBQQ__AdvancedCondition__c`: a boolean expression over condition indexes, e.g.
3
+ * `(1 AND 2) OR 3`. Verified grammar: AND and OR with parentheses, nesting to any depth, an index
4
+ * may appear more than once; NOT is rejected outright by CPQ.
5
+ */
6
+ export type ConditionExpr = {
7
+ kind: 'index';
8
+ index: number;
9
+ } | {
10
+ kind: 'and';
11
+ terms: ConditionExpr[];
12
+ } | {
13
+ kind: 'or';
14
+ terms: ConditionExpr[];
15
+ };
16
+ export interface ParsedCondition {
17
+ expr: ConditionExpr;
18
+ indexes: number[];
19
+ /** Disjunctive normal form: a list of AND-groups, any one of which satisfies the rule. */
20
+ dnf: number[][];
21
+ }
22
+ export declare class AdvancedConditionError extends Error {
23
+ constructor(message: string);
24
+ }
25
+ export declare function parseAdvancedCondition(src: string | null | undefined): ParsedCondition;
26
+ export declare function toDnf(e: ConditionExpr): number[][];
27
+ /** Validates the expression against the indexes actually present on the rule's conditions. */
28
+ export declare function validateAgainstIndexes(src: string | null | undefined, present: number[]): {
29
+ ok: boolean;
30
+ missing: number[];
31
+ error?: string;
32
+ parsed?: ParsedCondition;
33
+ };
34
+ //# sourceMappingURL=advancedCondition.d.ts.map
@@ -0,0 +1,112 @@
1
+ export class AdvancedConditionError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'AdvancedConditionError';
5
+ }
6
+ }
7
+ function tokenize(src) {
8
+ const tokens = [];
9
+ const re = /\s*(\d+|AND|OR|NOT|\(|\))\s*/giy;
10
+ let pos = 0;
11
+ while (pos < src.length) {
12
+ re.lastIndex = pos;
13
+ const m = re.exec(src);
14
+ if (!m || m.index !== pos)
15
+ throw new AdvancedConditionError(`unexpected text at position ${pos}: "${src.slice(pos, pos + 12)}"`);
16
+ const raw = m[1].toUpperCase();
17
+ if (raw === 'NOT')
18
+ throw new AdvancedConditionError('Only the following logical operators are accepted: AND, OR.');
19
+ if (raw === 'AND')
20
+ tokens.push({ t: 'and' });
21
+ else if (raw === 'OR')
22
+ tokens.push({ t: 'or' });
23
+ else if (raw === '(')
24
+ tokens.push({ t: '(' });
25
+ else if (raw === ')')
26
+ tokens.push({ t: ')' });
27
+ else
28
+ tokens.push({ t: 'num', v: Number(raw) });
29
+ pos = re.lastIndex;
30
+ }
31
+ return tokens;
32
+ }
33
+ export function parseAdvancedCondition(src) {
34
+ const text = (src ?? '').trim();
35
+ if (!text)
36
+ throw new AdvancedConditionError('empty expression');
37
+ const tokens = tokenize(text);
38
+ let i = 0;
39
+ const peek = () => tokens[i];
40
+ const next = () => tokens[i++];
41
+ function primary() {
42
+ const tok = next();
43
+ if (!tok)
44
+ throw new AdvancedConditionError('unexpected end of expression');
45
+ if (tok.t === 'num')
46
+ return { kind: 'index', index: tok.v };
47
+ if (tok.t === '(') {
48
+ const inner = orExpr();
49
+ const close = next();
50
+ if (close?.t !== ')')
51
+ throw new AdvancedConditionError('missing closing parenthesis');
52
+ return inner;
53
+ }
54
+ throw new AdvancedConditionError(`unexpected token ${tok.t}`);
55
+ }
56
+ function andExpr() {
57
+ const terms = [primary()];
58
+ while (peek()?.t === 'and') {
59
+ next();
60
+ terms.push(primary());
61
+ }
62
+ return terms.length === 1 ? terms[0] : { kind: 'and', terms };
63
+ }
64
+ function orExpr() {
65
+ const terms = [andExpr()];
66
+ while (peek()?.t === 'or') {
67
+ next();
68
+ terms.push(andExpr());
69
+ }
70
+ return terms.length === 1 ? terms[0] : { kind: 'or', terms };
71
+ }
72
+ const expr = orExpr();
73
+ if (i < tokens.length)
74
+ throw new AdvancedConditionError('unexpected trailing tokens');
75
+ const indexes = [...new Set(collect(expr))].sort((a, b) => a - b);
76
+ return { expr, indexes, dnf: toDnf(expr) };
77
+ }
78
+ function collect(e) {
79
+ if (e.kind === 'index')
80
+ return [e.index];
81
+ return e.terms.flatMap(collect);
82
+ }
83
+ export function toDnf(e) {
84
+ if (e.kind === 'index')
85
+ return [[e.index]];
86
+ if (e.kind === 'or')
87
+ return e.terms.flatMap(toDnf);
88
+ // AND: cartesian product of the children's DNF groups
89
+ let groups = [[]];
90
+ for (const term of e.terms) {
91
+ const sub = toDnf(term);
92
+ const nextGroups = [];
93
+ for (const g of groups)
94
+ for (const s of sub)
95
+ nextGroups.push([...new Set([...g, ...s])].sort((a, b) => a - b));
96
+ groups = nextGroups;
97
+ }
98
+ return groups;
99
+ }
100
+ /** Validates the expression against the indexes actually present on the rule's conditions. */
101
+ export function validateAgainstIndexes(src, present) {
102
+ try {
103
+ const parsed = parseAdvancedCondition(src);
104
+ const have = new Set(present);
105
+ const missing = parsed.indexes.filter((n) => !have.has(n));
106
+ return { ok: missing.length === 0, missing, parsed };
107
+ }
108
+ catch (err) {
109
+ return { ok: false, missing: [], error: err instanceof Error ? err.message : String(err) };
110
+ }
111
+ }
112
+ //# sourceMappingURL=advancedCondition.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Calculated-pricing formula grammar check. The report names the specific limit a formula breaches
3
+ * rather than saying "outside the grammar".
4
+ */
5
+ export declare const SUPPORTED_FUNCTIONS: readonly ["IF", "CASE", "AND", "OR", "NOT", "ISBLANK", "BLANKVALUE", "MIN", "MAX"];
6
+ export declare const AND_OR_MIN_ARGS = 2;
7
+ export declare const AND_OR_MAX_ARGS = 5;
8
+ export declare const MAX_NESTING = 10;
9
+ export declare const MAX_LENGTH = 3900;
10
+ export declare const MAX_IF_COUNT = 20;
11
+ export interface FormulaCheck {
12
+ ok: boolean;
13
+ breaches: string[];
14
+ functions: string[];
15
+ }
16
+ interface Call {
17
+ name: string;
18
+ args: number;
19
+ depth: number;
20
+ }
21
+ /** Parses function calls, counting arguments at the top level of each call. */
22
+ export declare function parseCalls(formula: string): {
23
+ calls: Call[];
24
+ maxDepth: number;
25
+ balanced: boolean;
26
+ };
27
+ export declare function checkFormula(formula: string | null | undefined): FormulaCheck;
28
+ export {};
29
+ //# sourceMappingURL=formula.d.ts.map
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Calculated-pricing formula grammar check. The report names the specific limit a formula breaches
3
+ * rather than saying "outside the grammar".
4
+ */
5
+ export const SUPPORTED_FUNCTIONS = [
6
+ 'IF',
7
+ 'CASE',
8
+ 'AND',
9
+ 'OR',
10
+ 'NOT',
11
+ 'ISBLANK',
12
+ 'BLANKVALUE',
13
+ 'MIN',
14
+ 'MAX',
15
+ ];
16
+ export const AND_OR_MIN_ARGS = 2;
17
+ export const AND_OR_MAX_ARGS = 5;
18
+ export const MAX_NESTING = 10;
19
+ export const MAX_LENGTH = 3_900;
20
+ export const MAX_IF_COUNT = 20;
21
+ /** Parses function calls, counting arguments at the top level of each call. */
22
+ export function parseCalls(formula) {
23
+ const calls = [];
24
+ const stack = [];
25
+ let depth = 0;
26
+ let maxDepth = 0;
27
+ let i = 0;
28
+ let inString = null;
29
+ while (i < formula.length) {
30
+ const ch = formula[i];
31
+ if (inString) {
32
+ if (ch === '\\')
33
+ i += 1;
34
+ else if (ch === inString)
35
+ inString = null;
36
+ i += 1;
37
+ continue;
38
+ }
39
+ if (ch === '"' || ch === "'") {
40
+ inString = ch;
41
+ const top = stack[stack.length - 1];
42
+ if (top)
43
+ top.hasContent = true;
44
+ i += 1;
45
+ continue;
46
+ }
47
+ const ident = /^[A-Za-z_][A-Za-z0-9_]*/.exec(formula.slice(i));
48
+ if (ident && /^\s*\(/.test(formula.slice(i + ident[0].length))) {
49
+ const name = ident[0].toUpperCase();
50
+ const parenAt = formula.indexOf('(', i + ident[0].length);
51
+ const top = stack[stack.length - 1];
52
+ if (top)
53
+ top.hasContent = true;
54
+ stack.push({ name, args: 0, hasContent: false });
55
+ depth += 1;
56
+ maxDepth = Math.max(maxDepth, depth);
57
+ i = parenAt + 1;
58
+ continue;
59
+ }
60
+ if (ident) {
61
+ const top = stack[stack.length - 1];
62
+ if (top)
63
+ top.hasContent = true;
64
+ i += ident[0].length;
65
+ continue;
66
+ }
67
+ if (ch === '(') {
68
+ stack.push({ name: '(', args: 0, hasContent: false });
69
+ depth += 1;
70
+ maxDepth = Math.max(maxDepth, depth);
71
+ }
72
+ else if (ch === ')') {
73
+ const top = stack.pop();
74
+ depth = Math.max(0, depth - 1);
75
+ if (top && top.name !== '(') {
76
+ calls.push({ name: top.name, args: top.hasContent ? top.args + 1 : 0, depth });
77
+ }
78
+ const parent = stack[stack.length - 1];
79
+ if (parent)
80
+ parent.hasContent = true;
81
+ }
82
+ else if (ch === ',') {
83
+ const top = stack[stack.length - 1];
84
+ if (top && top.name !== '(')
85
+ top.args += 1;
86
+ }
87
+ else if (!/\s/.test(ch)) {
88
+ const top = stack[stack.length - 1];
89
+ if (top)
90
+ top.hasContent = true;
91
+ }
92
+ i += 1;
93
+ }
94
+ return { calls, maxDepth, balanced: stack.length === 0 && !inString };
95
+ }
96
+ export function checkFormula(formula) {
97
+ const text = (formula ?? '').trim();
98
+ const breaches = [];
99
+ if (!text)
100
+ return { ok: true, breaches, functions: [] };
101
+ const { calls, maxDepth, balanced } = parseCalls(text);
102
+ const functions = [...new Set(calls.map((c) => c.name))];
103
+ if (!balanced)
104
+ breaches.push('unbalanced parentheses or quotes');
105
+ if (text.length > MAX_LENGTH)
106
+ breaches.push(`${text.length} characters, over the ${MAX_LENGTH}-character limit`);
107
+ if (maxDepth > MAX_NESTING)
108
+ breaches.push(`nested ${maxDepth} levels deep, over the limit of ${MAX_NESTING}`);
109
+ const ifs = calls.filter((c) => c.name === 'IF').length;
110
+ if (ifs > MAX_IF_COUNT)
111
+ breaches.push(`${ifs} IF calls, over the limit of ${MAX_IF_COUNT}`);
112
+ const supported = new Set(SUPPORTED_FUNCTIONS);
113
+ for (const fn of functions) {
114
+ if (!supported.has(fn))
115
+ breaches.push(`${fn}() is not in the calculated pricing grammar`);
116
+ }
117
+ for (const c of calls) {
118
+ if ((c.name === 'AND' || c.name === 'OR') &&
119
+ (c.args < AND_OR_MIN_ARGS || c.args > AND_OR_MAX_ARGS)) {
120
+ breaches.push(`${c.name}() takes ${AND_OR_MIN_ARGS} to ${AND_OR_MAX_ARGS} arguments; this one has ${c.args}`);
121
+ }
122
+ }
123
+ return { ok: breaches.length === 0, breaches: [...new Set(breaches)], functions };
124
+ }
125
+ //# sourceMappingURL=formula.js.map
@@ -0,0 +1,80 @@
1
+ import type { Extraction } from '../extract/index.js';
2
+ import { type BulkTouch } from '../liveness.js';
3
+ import type { ObjectRead } from '../soql.js';
4
+ import type { BucketId } from '../types.js';
5
+ import { type RowId } from './rules.js';
6
+ export interface Listing {
7
+ rowId: RowId;
8
+ bucket: BucketId;
9
+ }
10
+ export interface ClassifiedRecord {
11
+ id: string;
12
+ object: string;
13
+ name: string;
14
+ primary: Listing;
15
+ secondary: Listing[];
16
+ alive: boolean;
17
+ /** Free-text facts the report can print (formula breach, hooks implemented, ...). */
18
+ details: string[];
19
+ }
20
+ /** Count-only rows: the tool has the aggregate but not the records (contracts, quotes, currencies). */
21
+ export interface Tally {
22
+ rowId: RowId;
23
+ bucket: BucketId;
24
+ count: number;
25
+ alive: number;
26
+ details?: string[];
27
+ }
28
+ export interface ObjectStat {
29
+ object: string;
30
+ status: ObjectRead['status'];
31
+ count: number;
32
+ retrieved: number;
33
+ classified: number;
34
+ residue: number;
35
+ partial: boolean;
36
+ bulkTouch: BulkTouch | null;
37
+ }
38
+ export interface Classification {
39
+ records: ClassifiedRecord[];
40
+ tallies: Tally[];
41
+ residue: {
42
+ object: string;
43
+ id: string;
44
+ name: string;
45
+ }[];
46
+ objects: ObjectStat[];
47
+ facts: ClassificationFacts;
48
+ }
49
+ export interface ClassificationFacts {
50
+ bundles: number;
51
+ nestingDepth: number;
52
+ featureCount: number;
53
+ optionCount: number;
54
+ approverCount: number;
55
+ medianApprovalCycleDays: number | null;
56
+ approvalHistoryCount: number;
57
+ approvalRulesFired: number;
58
+ contractedPriceAccounts: number;
59
+ discountTierCount: number;
60
+ templateSectionCount: number;
61
+ templateContentCount: number;
62
+ deadSummaryVariables: number;
63
+ qcpHooks: Record<string, string[]>;
64
+ ruleConditionProblems: {
65
+ rule: string;
66
+ problem: string;
67
+ }[];
68
+ attributeCount: number;
69
+ currencies: string[];
70
+ multiCurrency: boolean;
71
+ advancedApprovalsInstalled: boolean;
72
+ nativeApprovalProcesses: number;
73
+ subscriptionCount: number;
74
+ productsNeverQuoted: number;
75
+ amendRenewShareLastYear: number | null;
76
+ lastYear: number | null;
77
+ }
78
+ export declare function classifyAll(ex: Extraction): Classification;
79
+ export declare function median(values: number[]): number | null;
80
+ //# sourceMappingURL=index.d.ts.map