@simplysf/simply-sobject-core 0.2.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.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @simplysf/simply-sobject-core
2
+
3
+ [![NPM](https://img.shields.io/npm/v/@simplysf/simply-sobject-core?label=@simplysf/simply-sobject-core)](https://npmjs.com/@simplysf/simply-sobject-core) [![Downloads/week](https://img.shields.io/npm/dw/@simplysf/simply-sobject-core.svg)](https://npmjs.com/@simplysf/simply-sobject-core) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://raw.githubusercontent.com/SimplySF/simply-node/main/LICENSE.txt)
4
+
5
+ Field history object derivation/filtering and relationship-field discovery. This is not a Salesforce CLI plugin — it's the library layer behind [`@simplysf/simply-sobject`](https://github.com/SimplySF/simply-plugins/tree/main/packages/simply-sobject)'s field-history and backup commands, published separately so it can be imported directly by anything that wants the same logic (an editor extension, a CI job, a script) without pulling in the CLI framework.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @simplysf/simply-sobject-core
11
+ ```
12
+
13
+ Requires Node.js `>=22` and either `"type": "module"` or a dynamic `import()` — this package ships ESM only.
14
+
15
+ ## API
16
+
17
+ Everything below is exported from the package root. Removing or renaming an export is a breaking change; see [`src/index.ts`](src/index.ts).
18
+
19
+ | Export | Description |
20
+ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
21
+ | `getHistoryObjectName(sobject)` | Derives a field history object's API name for a tracked sobject (e.g. `Account` → `AccountHistory`). |
22
+ | `getParentIdField(sobject)` | Derives a field history object's lookup field back to its parent record. |
23
+ | `buildWhereClause(filter, parentFieldName, soqlFilterableFields)` | Builds a SOQL WHERE clause from the SOQL-filterable subset of a filter tree. |
24
+ | `recordMatchesClientFilters(record, filter, parentFieldName, soqlFilterableFields)` | Evaluates the non-SOQL-filterable subset of a filter tree against a queried record. |
25
+ | `discoverRelationshipFields(connection, fields)` | Discovers identifying-field relationship paths (e.g. `RecordType.Name`) for an SObject's describe result. |
26
+ | `buildFieldHistorySchemaReportHtml(options)` | Pure function: renders a self-contained HTML report of field-history-tracked objects/fields. |
27
+ | `FilterConditionSchema`, `FilterGroupSchema`, `FilterConfigSchema` | zod schemas for parsing/validating a filter tree (the JSON shape `simply sobject history` commands' `--filter` flag accepts). |
28
+ | `FilterCondition`, `FilterGroup`, `FilterConfig`, `FieldHistorySchemaEntry`, `GroupedFieldHistorySchemaData` | Supporting types. |
29
+
30
+ ```ts
31
+ import { getHistoryObjectName, getParentIdField } from '@simplysf/simply-sobject-core';
32
+
33
+ getHistoryObjectName('Opportunity'); // 'OpportunityFieldHistory'
34
+ getParentIdField('My_Object__c'); // 'ParentId'
35
+ ```
36
+
37
+ ```ts
38
+ import { FilterConfigSchema, buildWhereClause, recordMatchesClientFilters } from '@simplysf/simply-sobject-core';
39
+
40
+ const filter = FilterConfigSchema.parse(JSON.parse(rawJson));
41
+ const whereClause = buildWhereClause(filter, 'AccountId', new Set(['AccountId', 'CreatedDate']));
42
+ // ...run the query, then for each returned record:
43
+ const included = recordMatchesClientFilters(record, filter, 'AccountId', new Set(['AccountId', 'CreatedDate']));
44
+ ```
45
+
46
+ ```ts
47
+ import { discoverRelationshipFields } from '@simplysf/simply-sobject-core';
48
+
49
+ const describeResult = await connection.describe('Account');
50
+ const relationshipPaths = await discoverRelationshipFields(connection, describeResult.fields);
51
+ ```
52
+
53
+ ## Issues
54
+
55
+ Please report any issues at https://github.com/SimplySF/simply-node/issues
56
+
57
+ ## Contributing
58
+
59
+ This package is part of the [`@simplysf/simply`](https://github.com/SimplySF/simply-node) monorepo. See [CONTRIBUTING.md](CONTRIBUTING.md) for what's specific to this package, and the repo's [root CONTRIBUTING.md](https://github.com/SimplySF/simply-node/blob/main/CONTRIBUTING.md) for repo structure, setup, commit conventions, and how to submit a pull request. Please also read our [Code of Conduct](https://github.com/SimplySF/simply-node/blob/main/CODE_OF_CONDUCT.md).
60
+
61
+ ## License
62
+
63
+ Licensed under the [Apache-2.0](https://raw.githubusercontent.com/SimplySF/simply-node/main/LICENSE.txt) license.
@@ -0,0 +1,43 @@
1
+ import type { FilterGroup } from './schemas/history/filterConfig.js';
2
+ /**
3
+ * Derive the field history object's API name for an sobject. Opportunity is a special case —
4
+ * its history object is `OpportunityFieldHistory`, not `OpportunityHistory`.
5
+ *
6
+ * @param sobject - The tracked sobject's API name.
7
+ * @returns The corresponding field history object's API name.
8
+ */
9
+ export declare function getHistoryObjectName(sobject: string): string;
10
+ /**
11
+ * Derive the field history object's lookup field back to the parent record.
12
+ *
13
+ * @param sobject - The tracked sobject's API name.
14
+ * @returns The field history object's lookup field name (e.g. `AccountId`, `ParentId`).
15
+ */
16
+ export declare function getParentIdField(sobject: string): string;
17
+ /**
18
+ * Build a SOQL WHERE clause from the subset of a filter tree that references SOQL-filterable
19
+ * fields. Conditions on any other field are left for {@link recordMatchesClientFilters} to
20
+ * apply after the query runs.
21
+ *
22
+ * @param node - The filter tree (or subtree) to build a clause from.
23
+ * @param parentFieldName - The history object's real parent lookup field, substituted for the
24
+ * `'ParentId'` placeholder.
25
+ * @param soqlFilterableFields - Fields that can be safely pushed into the SOQL WHERE clause;
26
+ * conditions on any other field are omitted here.
27
+ * @returns The WHERE clause body (no leading `WHERE`), or `''` if nothing was filterable.
28
+ */
29
+ export declare function buildWhereClause(node: FilterGroup | undefined, parentFieldName: string, soqlFilterableFields: ReadonlySet<string>): string;
30
+ /**
31
+ * Evaluate whether a queried record satisfies the parts of a filter tree that reference fields
32
+ * *not* covered by the SOQL WHERE clause (e.g. OldValue/NewValue). Conditions on SOQL-filterable
33
+ * fields are treated as already satisfied, since the query itself enforced them.
34
+ *
35
+ * @param record - The queried record to test.
36
+ * @param node - The filter tree (or subtree) to evaluate.
37
+ * @param parentFieldName - The history object's real parent lookup field, substituted for the
38
+ * `'ParentId'` placeholder.
39
+ * @param soqlFilterableFields - Fields already enforced by the SOQL WHERE clause; conditions on
40
+ * these fields are treated as satisfied without re-checking.
41
+ * @returns Whether `record` satisfies the non-SOQL-filterable parts of `node`.
42
+ */
43
+ export declare function recordMatchesClientFilters(record: Record<string, string>, node: FilterGroup | undefined, parentFieldName: string, soqlFilterableFields: ReadonlySet<string>): boolean;
@@ -0,0 +1,187 @@
1
+ /*
2
+ * Copyright (c) 2026, Clay Chipps.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * Derive the field history object's API name for an sobject. Opportunity is a special case —
18
+ * its history object is `OpportunityFieldHistory`, not `OpportunityHistory`.
19
+ *
20
+ * @param sobject - The tracked sobject's API name.
21
+ * @returns The corresponding field history object's API name.
22
+ */
23
+ export function getHistoryObjectName(sobject) {
24
+ if (sobject === 'Opportunity') {
25
+ return 'OpportunityFieldHistory';
26
+ }
27
+ if (sobject.endsWith('__c')) {
28
+ return `${sobject.slice(0, -3)}__History`;
29
+ }
30
+ return `${sobject}History`;
31
+ }
32
+ /**
33
+ * Derive the field history object's lookup field back to the parent record.
34
+ *
35
+ * @param sobject - The tracked sobject's API name.
36
+ * @returns The field history object's lookup field name (e.g. `AccountId`, `ParentId`).
37
+ */
38
+ export function getParentIdField(sobject) {
39
+ if (sobject === 'Opportunity') {
40
+ return 'OpportunityId';
41
+ }
42
+ if (sobject.endsWith('__c')) {
43
+ return 'ParentId';
44
+ }
45
+ return `${sobject}Id`;
46
+ }
47
+ /** Type guard distinguishing a nested {@link FilterGroup} from a leaf {@link FilterCondition}. */
48
+ function isFilterGroup(filter) {
49
+ return 'logic' in filter;
50
+ }
51
+ /**
52
+ * @param field - The filter's configured field name; `'ParentId'` is a placeholder for the
53
+ * history object's actual parent lookup field.
54
+ * @param parentFieldName - The history object's real parent lookup field (e.g. `AccountId`).
55
+ * @returns `parentFieldName` if `field` is the `'ParentId'` placeholder, otherwise `field` as-is.
56
+ */
57
+ function resolveFieldName(field, parentFieldName) {
58
+ return field === 'ParentId' ? parentFieldName : field;
59
+ }
60
+ /**
61
+ * @param operator - The filter condition's operator.
62
+ * @param value - The filter condition's configured value.
63
+ * @param isDateField - Whether the field being compared is a date/datetime field (affects string
64
+ * quoting, since date literals shouldn't be quoted).
65
+ * @returns The value formatted as a SOQL literal, ready to interpolate into a WHERE clause.
66
+ */
67
+ function formatSoqlValue(operator, value, isDateField) {
68
+ if (operator === 'IN' || operator === 'NOT IN') {
69
+ const values = Array.isArray(value) ? value : [value];
70
+ return `(${values.map((v) => (typeof v === 'string' ? `'${v}'` : String(v))).join(',')})`;
71
+ }
72
+ if (typeof value === 'string' && !isDateField) {
73
+ return `'${value}'`;
74
+ }
75
+ return String(value);
76
+ }
77
+ /**
78
+ * Build a SOQL WHERE clause from the subset of a filter tree that references SOQL-filterable
79
+ * fields. Conditions on any other field are left for {@link recordMatchesClientFilters} to
80
+ * apply after the query runs.
81
+ *
82
+ * @param node - The filter tree (or subtree) to build a clause from.
83
+ * @param parentFieldName - The history object's real parent lookup field, substituted for the
84
+ * `'ParentId'` placeholder.
85
+ * @param soqlFilterableFields - Fields that can be safely pushed into the SOQL WHERE clause;
86
+ * conditions on any other field are omitted here.
87
+ * @returns The WHERE clause body (no leading `WHERE`), or `''` if nothing was filterable.
88
+ */
89
+ export function buildWhereClause(node, parentFieldName, soqlFilterableFields) {
90
+ if (!node || node.filters.length === 0) {
91
+ return '';
92
+ }
93
+ const clause = node.filters
94
+ .map((filter) => {
95
+ if (isFilterGroup(filter)) {
96
+ const nested = buildWhereClause(filter, parentFieldName, soqlFilterableFields);
97
+ return nested ? `(${nested})` : '';
98
+ }
99
+ const field = resolveFieldName(filter.field, parentFieldName);
100
+ if (!soqlFilterableFields.has(field)) {
101
+ return '';
102
+ }
103
+ const value = formatSoqlValue(filter.operator, filter.value, field === 'CreatedDate');
104
+ return `${field} ${filter.operator} ${value}`;
105
+ })
106
+ .filter((part) => part.length > 0)
107
+ .join(` ${node.logic} `);
108
+ return clause;
109
+ }
110
+ /**
111
+ * @param pattern - A SOQL `LIKE` pattern (`%` as the wildcard).
112
+ * @returns An equivalent `RegExp` source string, with regex metacharacters escaped and `%`
113
+ * translated to `.*`.
114
+ */
115
+ function escapeLikePattern(pattern) {
116
+ return pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/%/g, '.*');
117
+ }
118
+ /**
119
+ * @param recordValue - The record's (string-valued) field value.
120
+ * @param filterValue - The filter condition's configured value.
121
+ * @param operator - The filter condition's operator.
122
+ * @returns Whether the comparison holds. Numeric-looking values are compared numerically.
123
+ */
124
+ function compareValues(recordValue, filterValue, operator) {
125
+ if (operator === 'IN' || operator === 'NOT IN') {
126
+ const values = Array.isArray(filterValue) ? filterValue : [filterValue];
127
+ const isMember = values.some((value) => String(value) === recordValue);
128
+ return operator === 'IN' ? isMember : !isMember;
129
+ }
130
+ if (operator === 'LIKE') {
131
+ return new RegExp(`^${escapeLikePattern(String(filterValue))}$`).test(recordValue ?? '');
132
+ }
133
+ // record values are always strings (queryRecords() normalizes both the REST and Bulk API
134
+ // paths to strings), so numeric/date comparisons need explicit coercion back to numbers —
135
+ // otherwise e.g. "9" > "10" would be true, and "5" === 5 would never match.
136
+ const numericRecord = recordValue !== undefined && recordValue !== '' ? Number(recordValue) : NaN;
137
+ const numericFilter = typeof filterValue === 'number' ? filterValue : Number(filterValue);
138
+ const bothNumeric = !Number.isNaN(numericRecord) && !Number.isNaN(numericFilter);
139
+ const left = bothNumeric ? numericRecord : (recordValue ?? '');
140
+ const right = bothNumeric ? numericFilter : String(filterValue);
141
+ switch (operator) {
142
+ case '=':
143
+ return left === right;
144
+ case '!=':
145
+ return left !== right;
146
+ case '>':
147
+ return left > right;
148
+ case '<':
149
+ return left < right;
150
+ case '>=':
151
+ return left >= right;
152
+ case '<=':
153
+ return left <= right;
154
+ default:
155
+ return false;
156
+ }
157
+ }
158
+ /**
159
+ * Evaluate whether a queried record satisfies the parts of a filter tree that reference fields
160
+ * *not* covered by the SOQL WHERE clause (e.g. OldValue/NewValue). Conditions on SOQL-filterable
161
+ * fields are treated as already satisfied, since the query itself enforced them.
162
+ *
163
+ * @param record - The queried record to test.
164
+ * @param node - The filter tree (or subtree) to evaluate.
165
+ * @param parentFieldName - The history object's real parent lookup field, substituted for the
166
+ * `'ParentId'` placeholder.
167
+ * @param soqlFilterableFields - Fields already enforced by the SOQL WHERE clause; conditions on
168
+ * these fields are treated as satisfied without re-checking.
169
+ * @returns Whether `record` satisfies the non-SOQL-filterable parts of `node`.
170
+ */
171
+ export function recordMatchesClientFilters(record, node, parentFieldName, soqlFilterableFields) {
172
+ if (!node || node.filters.length === 0) {
173
+ return true;
174
+ }
175
+ const matches = (filter) => {
176
+ if (isFilterGroup(filter)) {
177
+ return recordMatchesClientFilters(record, filter, parentFieldName, soqlFilterableFields);
178
+ }
179
+ const field = resolveFieldName(filter.field, parentFieldName);
180
+ if (soqlFilterableFields.has(field)) {
181
+ return true;
182
+ }
183
+ return compareValues(record[field], filter.value, filter.operator);
184
+ };
185
+ return node.logic === 'AND' ? node.filters.every(matches) : node.filters.some(matches);
186
+ }
187
+ //# sourceMappingURL=fieldHistory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldHistory.js","sourceRoot":"","sources":["../src/fieldHistory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /** A single field-history-tracked field, as rendered in the report. */
2
+ export type FieldHistorySchemaEntry = {
3
+ objectName: string;
4
+ objectApiName: string;
5
+ fieldName: string;
6
+ fieldApiName: string;
7
+ managedPackageNamespace: string;
8
+ packageName: string;
9
+ };
10
+ /** Tracked fields, grouped by owning package (namespace/subscriber package name, or `'Local (Unpackaged)'`). */
11
+ export type GroupedFieldHistorySchemaData = Map<string, FieldHistorySchemaEntry[]>;
12
+ /**
13
+ * Render a complete, self-contained HTML report of field-history-tracked objects/fields, grouped
14
+ * by owning package, with each package section collapsible and a client-side search box for
15
+ * filtering objects/fields across all packages.
16
+ *
17
+ * @param options.username - The org username the report was generated against.
18
+ * @param options.reportDate - The report generation date/time, displayed as-is.
19
+ * @param options.groupedData - The tracked fields to render, grouped by package.
20
+ * @returns The rendered HTML document.
21
+ */
22
+ export declare function buildFieldHistorySchemaReportHtml(options: {
23
+ username: string;
24
+ reportDate: string;
25
+ groupedData: GroupedFieldHistorySchemaData;
26
+ }): string;
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Copyright (c) 2026, Clay Chipps.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { BADGE_CSS, COLLAPSIBLE_SECTION_CSS, createReportHandlebars, renderReportPage } from '@simplysf/simply-report';
17
+ const handlebars = createReportHandlebars();
18
+ const fieldRowSource = `
19
+ <tr class="field-row">
20
+ <td><strong>{{objectName}}</strong><br><small>{{objectApiName}}</small></td>
21
+ <td><strong>{{fieldName}}</strong><br><small>{{fieldApiName}}</small></td>
22
+ <td>
23
+ {{#unless (eq managedPackageNamespace "N/A")}}<span class="badge badge-ns">{{managedPackageNamespace}}</span>{{/unless}}
24
+ <span class="badge">{{packageName}}</span>
25
+ </td>
26
+ </tr>`;
27
+ const packageSectionSource = `
28
+ <div class="package-section" data-package="{{name}}">
29
+ <h2>Package: {{name}}</h2>
30
+ <details>
31
+ <summary>Tracked Fields ({{fields.length}})</summary>
32
+ <div class="content">
33
+ <table>
34
+ <thead><tr><th>Object</th><th>Field</th><th>Details</th></tr></thead>
35
+ <tbody>{{#each fields}}{{> fieldRow}}{{/each}}</tbody>
36
+ </table>
37
+ </div>
38
+ </details>
39
+ </div>`;
40
+ const reportSource = renderReportPage({
41
+ title: 'Field History Tracking Report',
42
+ css: [
43
+ COLLAPSIBLE_SECTION_CSS,
44
+ ' table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 0.9em; }',
45
+ ' th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }',
46
+ ' th { background-color: #f8f9fa; position: sticky; top: 0; }',
47
+ ' tr:hover { background-color: #f5f5f5; }',
48
+ BADGE_CSS,
49
+ ' .badge-ns { background-color: #3498db; }',
50
+ ' .search-container { margin-bottom: 20px; position: sticky; top: 10px; z-index: 100; }',
51
+ ' input[type="text"] { padding: 12px; width: 100%; max-width: 400px; border: 2px solid #3498db; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }',
52
+ ].join('\n'),
53
+ body: ` <h1>Field History Tracking Report</h1>
54
+ <p>Org: <strong>{{username}}</strong> | Tracked Fields: {{fieldCount}} | Generated: {{reportDate}}</p>
55
+ <div class="search-container">
56
+ <input type="text" id="searchInput" onkeyup="filterFields()" placeholder="Search objects or fields across all packages...">
57
+ </div>
58
+ {{#each packages}}{{> packageSection}}{{/each}}`,
59
+ script: ` function filterFields() {
60
+ const input = document.getElementById("searchInput");
61
+ const filter = input.value.toUpperCase();
62
+ const sections = document.querySelectorAll(".package-section");
63
+ sections.forEach(section => {
64
+ let sectionHasVisibleRows = false;
65
+ const rows = section.querySelectorAll(".field-row");
66
+ const details = section.querySelector("details");
67
+
68
+ rows.forEach(row => {
69
+ const text = row.textContent || row.innerText;
70
+ if (text.toUpperCase().indexOf(filter) > -1) {
71
+ row.style.display = "";
72
+ sectionHasVisibleRows = true;
73
+ } else {
74
+ row.style.display = "none";
75
+ }
76
+ });
77
+
78
+ section.style.display = sectionHasVisibleRows ? "" : "none";
79
+ if (filter && sectionHasVisibleRows) {
80
+ details.open = true;
81
+ } else if (!filter) {
82
+ details.open = false;
83
+ }
84
+ });
85
+ }`,
86
+ });
87
+ handlebars.registerPartial('fieldRow', fieldRowSource);
88
+ handlebars.registerPartial('packageSection', packageSectionSource);
89
+ const renderReport = handlebars.compile(reportSource);
90
+ /**
91
+ * Render a complete, self-contained HTML report of field-history-tracked objects/fields, grouped
92
+ * by owning package, with each package section collapsible and a client-side search box for
93
+ * filtering objects/fields across all packages.
94
+ *
95
+ * @param options.username - The org username the report was generated against.
96
+ * @param options.reportDate - The report generation date/time, displayed as-is.
97
+ * @param options.groupedData - The tracked fields to render, grouped by package.
98
+ * @returns The rendered HTML document.
99
+ */
100
+ export function buildFieldHistorySchemaReportHtml(options) {
101
+ const { username, reportDate, groupedData } = options;
102
+ const packages = [...groupedData.keys()]
103
+ .sort()
104
+ .map((name) => ({ name, fields: groupedData.get(name) ?? [] }));
105
+ const fieldCount = packages.reduce((sum, pkg) => sum + pkg.fields.length, 0);
106
+ return renderReport({ username, reportDate, fieldCount, packages });
107
+ }
108
+ //# sourceMappingURL=fieldHistorySchemaReportTemplate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldHistorySchemaReportTemplate.js","sourceRoot":"","sources":["../src/fieldHistorySchemaReportTemplate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,SAAS,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAevH,MAAM,UAAU,GAAG,sBAAsB,EAAE,CAAC;AAE5C,MAAM,cAAc,GAAG;;;;;;;;MAQjB,CAAC;AAEP,MAAM,oBAAoB,GAAG;;;;;;;;;;;;OAYtB,CAAC;AAER,MAAM,YAAY,GAAG,gBAAgB,CAAC;IACpC,KAAK,EAAE,+BAA+B;IACtC,GAAG,EAAE;QACH,uBAAuB;QACvB,yFAAyF;QACzF,uEAAuE;QACvE,+DAA+D;QAC/D,2CAA2C;QAC3C,SAAS;QACT,4CAA4C;QAC5C,yFAAyF;QACzF,8JAA8J;KAC/J,CAAC,IAAI,CAAC,IAAI,CAAC;IACZ,IAAI,EAAE;;;;;kDAK0C;IAChD,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;MA0BJ;CACL,CAAC,CAAC;AAEH,UAAU,CAAC,eAAe,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;AACvD,UAAU,CAAC,eAAe,CAAC,gBAAgB,EAAE,oBAAoB,CAAC,CAAC;AAEnE,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;AAQtD;;;;;;;;;GASG;AACH,MAAM,UAAU,iCAAiC,CAAC,OAIjD;IACC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;IAEtD,MAAM,QAAQ,GAAqB,CAAC,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC;SACvD,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;IAElE,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAE7E,OAAO,YAAY,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;AACtE,CAAC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { getHistoryObjectName, getParentIdField, buildWhereClause, recordMatchesClientFilters, } from './fieldHistory.js';
2
+ export { buildFieldHistorySchemaReportHtml, type FieldHistorySchemaEntry, type GroupedFieldHistorySchemaData, } from './fieldHistorySchemaReportTemplate.js';
3
+ export { discoverRelationshipFields } from './relationshipFields.js';
4
+ export { FilterConditionSchema, FilterGroupSchema, FilterConfigSchema, type FilterCondition, type FilterGroup, type FilterConfig, } from './schemas/history/filterConfig.js';
package/lib/index.js ADDED
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright (c) 2026, Clay Chipps.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ // Everything exported from this file is this package's public API and is semver-covered: adding an
17
+ // export is a minor/patch change, but removing or renaming one is breaking. `test/index.test.ts`
18
+ // pins the exported-key list down so an accidental removal fails a test instead of silently shipping
19
+ // in a patch release. See docs/design/0029-simply-sobject-core.md for why this package is split out
20
+ // from `@simplysf/simply-sobject` (the CLI) rather than being one more relative import inside it.
21
+ export { getHistoryObjectName, getParentIdField, buildWhereClause, recordMatchesClientFilters, } from './fieldHistory.js';
22
+ export { buildFieldHistorySchemaReportHtml, } from './fieldHistorySchemaReportTemplate.js';
23
+ export { discoverRelationshipFields } from './relationshipFields.js';
24
+ export { FilterConditionSchema, FilterGroupSchema, FilterConfigSchema, } from './schemas/history/filterConfig.js';
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,mGAAmG;AACnG,iGAAiG;AACjG,qGAAqG;AACrG,oGAAoG;AACpG,kGAAkG;AAElG,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,gBAAgB,EAChB,0BAA0B,GAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,iCAAiC,GAGlC,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,GAInB,MAAM,mCAAmC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { Connection } from '@salesforce/core';
2
+ type SObjectField = Awaited<ReturnType<Connection['describe']>>['fields'][number];
3
+ /**
4
+ * Given an SObject's describe fields, discover the identifying fields of every parent it
5
+ * references through a single-target lookup or master-detail relationship, and return them as
6
+ * dot-notation relationship paths ready to add to a SOQL field list (e.g. `RecordTypeId` ->
7
+ * `RecordType.Name`, `RecordType.DeveloperName`).
8
+ *
9
+ * Describe calls for each distinct target object are deduplicated and fetched concurrently;
10
+ * `Connection#describe` already memoizes per-instance, so repeat calls for the same object name
11
+ * (including calls made elsewhere on the same connection) are cheap.
12
+ *
13
+ * @param connection - The org connection to describe relationship targets with.
14
+ * @param fields - The describe fields of the SObject being backed up.
15
+ * @returns Dot-notation relationship field paths, in no particular order.
16
+ */
17
+ export declare function discoverRelationshipFields(connection: Connection, fields: SObjectField[]): Promise<string[]>;
18
+ export {};
@@ -0,0 +1,64 @@
1
+ /*
2
+ * Copyright (c) 2026, Clay Chipps.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * @param field - A field from an SObject describe result.
18
+ * @returns Whether `field` is a single-target lookup/master-detail field that can be safely
19
+ * expanded into a dot-notation relationship path. Polymorphic references (e.g. `OwnerId`, which
20
+ * can point at `User` or `Group`) are excluded, since a single dotted path can't reliably select
21
+ * fields across more than one possible target type.
22
+ */
23
+ function isExpandableReferenceField(field) {
24
+ return field.type === 'reference' && Boolean(field.relationshipName) && field.referenceTo?.length === 1;
25
+ }
26
+ /**
27
+ * @param targetDescribe - The describe result of a relationship field's target SObject.
28
+ * @returns The target object's identifying field names: whichever field is flagged as its name
29
+ * field (e.g. `Name`), plus `DeveloperName` when present. For `RecordType`, this yields `Name`
30
+ * and `DeveloperName`; for most other objects, just `Name`.
31
+ */
32
+ function identifyingFieldNames(targetDescribe) {
33
+ return targetDescribe.fields
34
+ .filter((field) => field.nameField || field.name === 'DeveloperName')
35
+ .map((field) => field.name);
36
+ }
37
+ /**
38
+ * Given an SObject's describe fields, discover the identifying fields of every parent it
39
+ * references through a single-target lookup or master-detail relationship, and return them as
40
+ * dot-notation relationship paths ready to add to a SOQL field list (e.g. `RecordTypeId` ->
41
+ * `RecordType.Name`, `RecordType.DeveloperName`).
42
+ *
43
+ * Describe calls for each distinct target object are deduplicated and fetched concurrently;
44
+ * `Connection#describe` already memoizes per-instance, so repeat calls for the same object name
45
+ * (including calls made elsewhere on the same connection) are cheap.
46
+ *
47
+ * @param connection - The org connection to describe relationship targets with.
48
+ * @param fields - The describe fields of the SObject being backed up.
49
+ * @returns Dot-notation relationship field paths, in no particular order.
50
+ */
51
+ export async function discoverRelationshipFields(connection, fields) {
52
+ const referenceFields = fields.filter(isExpandableReferenceField);
53
+ const targetObjectNames = [...new Set(referenceFields.map((field) => field.referenceTo[0]))];
54
+ const targetDescribes = await Promise.all(targetObjectNames.map(async (objectName) => [objectName, await connection.describe(objectName)]));
55
+ const describeByObjectName = new Map(targetDescribes);
56
+ return referenceFields.flatMap((field) => {
57
+ const targetDescribe = describeByObjectName.get(field.referenceTo[0]);
58
+ if (!targetDescribe) {
59
+ return [];
60
+ }
61
+ return identifyingFieldNames(targetDescribe).map((fieldName) => `${field.relationshipName}.${fieldName}`);
62
+ });
63
+ }
64
+ //# sourceMappingURL=relationshipFields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relationshipFields.js","sourceRoot":"","sources":["../src/relationshipFields.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH;;;;;;GAMG;AACH,SAAS,0BAA0B,CAAC,KAAmB;IACrD,OAAO,KAAK,CAAC,IAAI,KAAK,WAAW,IAAI,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,IAAI,KAAK,CAAC,WAAW,EAAE,MAAM,KAAK,CAAC,CAAC;AAC1G,CAAC;AAED;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,cAA2D;IACxF,OAAO,cAAc,CAAC,MAAM;SACzB,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,eAAe,CAAC;SACpE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAAC,UAAsB,EAAE,MAAsB;IAC7F,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC,0BAA0B,CAAC,CAAC;IAElE,MAAM,iBAAiB,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAE9F,MAAM,eAAe,GAAG,MAAM,OAAO,CAAC,GAAG,CACvC,iBAAiB,CAAC,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC,UAAU,EAAE,MAAM,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAU,CAAC,CAC1G,CAAC;IACF,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,CAAC;IAEtD,OAAO,eAAe,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;QACvC,MAAM,cAAc,GAAG,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,WAAY,CAAC,CAAC,CAAC,CAAC,CAAC;QAEvE,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO,qBAAqB,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,gBAAiB,IAAI,SAAS,EAAE,CAAC,CAAC;IAC7G,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ /** A single leaf filter condition: `field <operator> value`. */
3
+ export declare const FilterConditionSchema: z.ZodObject<{
4
+ field: z.ZodString;
5
+ operator: z.ZodEnum<{
6
+ "=": "=";
7
+ "!=": "!=";
8
+ ">": ">";
9
+ "<": "<";
10
+ ">=": ">=";
11
+ "<=": "<=";
12
+ IN: "IN";
13
+ "NOT IN": "NOT IN";
14
+ LIKE: "LIKE";
15
+ }>;
16
+ value: z.ZodUnknown;
17
+ }, z.core.$strip>;
18
+ /** A parsed, validated filter condition. */
19
+ export type FilterCondition = z.infer<typeof FilterConditionSchema>;
20
+ /** A group of filter conditions (and/or nested groups) combined with `AND`/`OR` logic. */
21
+ export type FilterGroup = {
22
+ logic: 'AND' | 'OR';
23
+ filters: Array<FilterCondition | FilterGroup>;
24
+ };
25
+ export declare const FilterGroupSchema: z.ZodType<FilterGroup>;
26
+ /** The top-level shape of the JSON config file consumed by `simply sobject history` commands' `--filter`. */
27
+ export type FilterConfig = FilterGroup;
28
+ /** Schema for {@link FilterConfig}. */
29
+ export declare const FilterConfigSchema: z.ZodType<FilterGroup, unknown, z.core.$ZodTypeInternals<FilterGroup, unknown>>;
@@ -0,0 +1,32 @@
1
+ /*
2
+ * Copyright (c) 2026, Clay Chipps.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { z } from 'zod';
17
+ /** A single leaf filter condition: `field <operator> value`. */
18
+ export const FilterConditionSchema = z.object({
19
+ field: z.string(),
20
+ operator: z.enum(['=', '!=', '>', '<', '>=', '<=', 'IN', 'NOT IN', 'LIKE']),
21
+ value: z.unknown(),
22
+ });
23
+ // Filter groups nest arbitrarily (a group's `filters` array can itself contain groups), so the
24
+ // schema has to reference itself. z.lazy() defers evaluation of the inner schema until it's
25
+ // actually used, which is what makes the self-reference possible.
26
+ export const FilterGroupSchema = z.lazy(() => z.object({
27
+ logic: z.enum(['AND', 'OR', 'and', 'or']).transform((value) => value.toUpperCase()),
28
+ filters: z.array(z.union([FilterConditionSchema, FilterGroupSchema])),
29
+ }));
30
+ /** Schema for {@link FilterConfig}. */
31
+ export const FilterConfigSchema = FilterGroupSchema;
32
+ //# sourceMappingURL=filterConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filterConfig.js","sourceRoot":"","sources":["../../../src/schemas/history/filterConfig.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,gEAAgE;AAChE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC3E,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE;CACnB,CAAC,CAAC;AAWH,+FAA+F;AAC/F,4FAA4F;AAC5F,kEAAkE;AAClE,MAAM,CAAC,MAAM,iBAAiB,GAA2B,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CACnE,CAAC,CAAC,MAAM,CAAC;IACP,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,EAAkB,CAAC;IACnG,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,qBAAqB,EAAE,iBAAiB,CAAC,CAAC,CAAC;CACtE,CAAC,CACH,CAAC;AAKF,uCAAuC;AACvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,iBAAiB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,134 @@
1
+ {
2
+ "name": "@simplysf/simply-sobject-core",
3
+ "description": "Field history object derivation/filtering and relationship-field discovery — the library layer behind @simplysf/simply-sobject, published separately for direct consumption (e.g. editor tooling, CI scripts) without the CLI framework",
4
+ "version": "0.2.0",
5
+ "author": "@ClayChipps",
6
+ "homepage": "https://github.com/SimplySF/simply-node",
7
+ "bugs": "https://github.com/SimplySF/simply-node/issues",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/SimplySF/simply-node",
11
+ "directory": "packages/simply-sobject-core"
12
+ },
13
+ "engines": {
14
+ "node": ">=22.0.0"
15
+ },
16
+ "files": [
17
+ "/lib"
18
+ ],
19
+ "keywords": [
20
+ "force",
21
+ "salesforce",
22
+ "sfdx",
23
+ "salesforcedx",
24
+ "sfdx-plugin",
25
+ "sf-plugin",
26
+ "sf",
27
+ "sobject"
28
+ ],
29
+ "license": "Apache-2.0",
30
+ "main": "./lib/index.js",
31
+ "types": "./lib/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./lib/index.d.ts",
35
+ "default": "./lib/index.js"
36
+ }
37
+ },
38
+ "type": "module",
39
+ "dependencies": {
40
+ "@salesforce/core": "^8.30.0",
41
+ "@simplysf/simply-report": "workspace:^1.0.4",
42
+ "zod": "^4.1.12"
43
+ },
44
+ "devDependencies": {
45
+ "@vitest/coverage-v8": "^4.1.10",
46
+ "vitest": "^4.1.10"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "scripts": {
52
+ "build": "wireit",
53
+ "compile": "wireit",
54
+ "fix-license": "eslint src test --fix --rule \"header/header: [2]\"",
55
+ "format": "wireit",
56
+ "lint": "wireit",
57
+ "test": "wireit",
58
+ "test:coverage": "vitest run --coverage --project simply-sobject-core",
59
+ "test:only": "wireit",
60
+ "test:watch": "vitest watch --project simply-sobject-core"
61
+ },
62
+ "wireit": {
63
+ "build": {
64
+ "dependencies": [
65
+ "compile",
66
+ "lint"
67
+ ]
68
+ },
69
+ "compile": {
70
+ "command": "tsc -p . --pretty --incremental",
71
+ "files": [
72
+ "src/**/*.ts",
73
+ "**/tsconfig.json",
74
+ "../../tsconfig.json"
75
+ ],
76
+ "output": [
77
+ "lib/**",
78
+ "*.tsbuildinfo"
79
+ ],
80
+ "clean": "if-file-deleted"
81
+ },
82
+ "format": {
83
+ "command": "prettier --write \"+(src|test)/**/*.+(ts|js|json)\"",
84
+ "files": [
85
+ "src/**/*.ts",
86
+ "test/**/*.ts",
87
+ ".prettier*"
88
+ ],
89
+ "output": []
90
+ },
91
+ "lint": {
92
+ "command": "eslint src test --color --cache --cache-location .eslintcache",
93
+ "files": [
94
+ "src/**/*.ts",
95
+ "test/**/*.ts",
96
+ "**/.eslint*",
97
+ "**/tsconfig.json",
98
+ "../../eslint.config.mjs",
99
+ "../../tsconfig.json"
100
+ ],
101
+ "output": []
102
+ },
103
+ "test": {
104
+ "dependencies": [
105
+ "test:compile",
106
+ "test:only",
107
+ "lint"
108
+ ]
109
+ },
110
+ "test:compile": {
111
+ "command": "tsc -p \"./test\" --pretty",
112
+ "files": [
113
+ "test/**/*.ts",
114
+ "**/tsconfig.json",
115
+ "../../tsconfig.json"
116
+ ],
117
+ "output": []
118
+ },
119
+ "test:only": {
120
+ "command": "vitest run --project simply-sobject-core",
121
+ "env": {
122
+ "FORCE_COLOR": "2"
123
+ },
124
+ "files": [
125
+ "test/**/*.ts",
126
+ "src/**/*.ts",
127
+ "**/tsconfig.json",
128
+ "../../tsconfig.json",
129
+ "vitest.config.ts"
130
+ ],
131
+ "output": []
132
+ }
133
+ }
134
+ }