@rippling/rippling-sdk 0.2.0-alpha.104 → 0.2.0-alpha.106
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/client.d.mts +12 -0
- package/client.d.mts.map +1 -1
- package/client.d.ts +12 -0
- package/client.d.ts.map +1 -1
- package/client.js +12 -0
- package/client.js.map +1 -1
- package/client.mjs +12 -0
- package/client.mjs.map +1 -1
- package/lib/charts/builders.d.mts +43 -0
- package/lib/charts/builders.d.mts.map +1 -0
- package/lib/charts/builders.d.ts +43 -0
- package/lib/charts/builders.d.ts.map +1 -0
- package/lib/charts/builders.js +77 -0
- package/lib/charts/builders.js.map +1 -0
- package/lib/charts/builders.mjs +70 -0
- package/lib/charts/builders.mjs.map +1 -0
- package/lib/charts/describe.d.mts +12 -0
- package/lib/charts/describe.d.mts.map +1 -0
- package/lib/charts/describe.d.ts +12 -0
- package/lib/charts/describe.d.ts.map +1 -0
- package/lib/charts/describe.js +64 -0
- package/lib/charts/describe.js.map +1 -0
- package/lib/charts/describe.mjs +61 -0
- package/lib/charts/describe.mjs.map +1 -0
- package/lib/charts/errors.d.mts +35 -0
- package/lib/charts/errors.d.mts.map +1 -0
- package/lib/charts/errors.d.ts +35 -0
- package/lib/charts/errors.d.ts.map +1 -0
- package/lib/charts/errors.js +55 -0
- package/lib/charts/errors.js.map +1 -0
- package/lib/charts/errors.mjs +47 -0
- package/lib/charts/errors.mjs.map +1 -0
- package/lib/charts/evaluate.d.mts +16 -0
- package/lib/charts/evaluate.d.mts.map +1 -0
- package/lib/charts/evaluate.d.ts +16 -0
- package/lib/charts/evaluate.d.ts.map +1 -0
- package/lib/charts/evaluate.js +99 -0
- package/lib/charts/evaluate.js.map +1 -0
- package/lib/charts/evaluate.mjs +96 -0
- package/lib/charts/evaluate.mjs.map +1 -0
- package/lib/charts/guards.d.mts +15 -0
- package/lib/charts/guards.d.mts.map +1 -0
- package/lib/charts/guards.d.ts +15 -0
- package/lib/charts/guards.d.ts.map +1 -0
- package/lib/charts/guards.js +38 -0
- package/lib/charts/guards.js.map +1 -0
- package/lib/charts/guards.mjs +34 -0
- package/lib/charts/guards.mjs.map +1 -0
- package/lib/charts/schema.d.mts +101 -0
- package/lib/charts/schema.d.mts.map +1 -0
- package/lib/charts/schema.d.ts +101 -0
- package/lib/charts/schema.d.ts.map +1 -0
- package/lib/charts/schema.js +74 -0
- package/lib/charts/schema.js.map +1 -0
- package/lib/charts/schema.mjs +71 -0
- package/lib/charts/schema.mjs.map +1 -0
- package/lib/charts/types.d.mts +79 -0
- package/lib/charts/types.d.mts.map +1 -0
- package/lib/charts/types.d.ts +79 -0
- package/lib/charts/types.d.ts.map +1 -0
- package/lib/charts/types.js +25 -0
- package/lib/charts/types.js.map +1 -0
- package/lib/charts/types.mjs +22 -0
- package/lib/charts/types.mjs.map +1 -0
- package/lib/charts.d.mts +7 -0
- package/lib/charts.d.mts.map +1 -0
- package/lib/charts.d.ts +7 -0
- package/lib/charts.d.ts.map +1 -0
- package/lib/charts.js +32 -0
- package/lib/charts.js.map +1 -0
- package/lib/charts.mjs +7 -0
- package/lib/charts.mjs.map +1 -0
- package/lib/manifest/_existing-component-fields.d.mts.map +1 -1
- package/lib/manifest/_existing-component-fields.d.ts.map +1 -1
- package/lib/manifest/_existing-component-fields.js +2 -0
- package/lib/manifest/_existing-component-fields.js.map +1 -1
- package/lib/manifest/_existing-component-fields.mjs +2 -0
- package/lib/manifest/_existing-component-fields.mjs.map +1 -1
- package/lib/manifest/existing-manifest-context.d.mts +3 -0
- package/lib/manifest/existing-manifest-context.d.mts.map +1 -1
- package/lib/manifest/existing-manifest-context.d.ts +3 -0
- package/lib/manifest/existing-manifest-context.d.ts.map +1 -1
- package/lib/manifest/existing-manifest-context.js +14 -0
- package/lib/manifest/existing-manifest-context.js.map +1 -1
- package/lib/manifest/existing-manifest-context.mjs +14 -0
- package/lib/manifest/existing-manifest-context.mjs.map +1 -1
- package/lib/manifest/workflow/index.d.mts +4 -0
- package/lib/manifest/workflow/index.d.mts.map +1 -1
- package/lib/manifest/workflow/index.d.ts +4 -0
- package/lib/manifest/workflow/index.d.ts.map +1 -1
- package/lib/manifest/workflow/index.js +49 -0
- package/lib/manifest/workflow/index.js.map +1 -1
- package/lib/manifest/workflow/index.mjs +49 -0
- package/lib/manifest/workflow/index.mjs.map +1 -1
- package/package.json +1 -1
- package/resources/activity-log.d.mts +517 -0
- package/resources/activity-log.d.mts.map +1 -0
- package/resources/activity-log.d.ts +517 -0
- package/resources/activity-log.d.ts.map +1 -0
- package/resources/activity-log.js +171 -0
- package/resources/activity-log.js.map +1 -0
- package/resources/activity-log.mjs +167 -0
- package/resources/activity-log.mjs.map +1 -0
- package/resources/compensations.d.mts +1 -1
- package/resources/compensations.d.ts +1 -1
- package/resources/compensations.js +1 -1
- package/resources/compensations.mjs +1 -1
- package/resources/developer-program-application.d.mts +1 -1
- package/resources/developer-program-application.d.ts +1 -1
- package/resources/developer-program-application.js +1 -1
- package/resources/developer-program-application.mjs +1 -1
- package/resources/developer-users.d.mts +2 -1
- package/resources/developer-users.d.mts.map +1 -1
- package/resources/developer-users.d.ts +2 -1
- package/resources/developer-users.d.ts.map +1 -1
- package/resources/developer-users.js +2 -1
- package/resources/developer-users.js.map +1 -1
- package/resources/developer-users.mjs +2 -1
- package/resources/developer-users.mjs.map +1 -1
- package/resources/hire-requirements.d.mts +8 -74
- package/resources/hire-requirements.d.mts.map +1 -1
- package/resources/hire-requirements.d.ts +8 -74
- package/resources/hire-requirements.d.ts.map +1 -1
- package/resources/hire-requirements.js +3 -19
- package/resources/hire-requirements.js.map +1 -1
- package/resources/hire-requirements.mjs +3 -19
- package/resources/hire-requirements.mjs.map +1 -1
- package/resources/hires.d.mts +677 -142
- package/resources/hires.d.mts.map +1 -1
- package/resources/hires.d.ts +677 -142
- package/resources/hires.d.ts.map +1 -1
- package/resources/hires.js +3 -4
- package/resources/hires.js.map +1 -1
- package/resources/hires.mjs +3 -4
- package/resources/hires.mjs.map +1 -1
- package/resources/index.d.mts +2 -0
- package/resources/index.d.mts.map +1 -1
- package/resources/index.d.ts +2 -0
- package/resources/index.d.ts.map +1 -1
- package/resources/index.js +7 -2
- package/resources/index.js.map +1 -1
- package/resources/index.mjs +2 -0
- package/resources/index.mjs.map +1 -1
- package/resources/listing-submissions.d.mts +1 -1
- package/resources/listing-submissions.d.ts +1 -1
- package/resources/listing-submissions.js +1 -1
- package/resources/listing-submissions.mjs +1 -1
- package/resources/listing-transitions.d.mts +1 -1
- package/resources/listing-transitions.d.ts +1 -1
- package/resources/listing-transitions.js +1 -1
- package/resources/listing-transitions.mjs +1 -1
- package/resources/listing.d.mts +1 -1
- package/resources/listing.d.ts +1 -1
- package/resources/listing.js +1 -1
- package/resources/listing.mjs +1 -1
- package/resources/payroll-payment-mode-changes.d.mts +102 -0
- package/resources/payroll-payment-mode-changes.d.mts.map +1 -0
- package/resources/payroll-payment-mode-changes.d.ts +102 -0
- package/resources/payroll-payment-mode-changes.d.ts.map +1 -0
- package/resources/payroll-payment-mode-changes.js +54 -0
- package/resources/payroll-payment-mode-changes.js.map +1 -0
- package/resources/payroll-payment-mode-changes.mjs +50 -0
- package/resources/payroll-payment-mode-changes.mjs.map +1 -0
- package/resources/payroll-runs/index.d.mts +1 -0
- package/resources/payroll-runs/index.d.mts.map +1 -1
- package/resources/payroll-runs/index.d.ts +1 -0
- package/resources/payroll-runs/index.d.ts.map +1 -1
- package/resources/payroll-runs/index.js +3 -1
- package/resources/payroll-runs/index.js.map +1 -1
- package/resources/payroll-runs/index.mjs +1 -0
- package/resources/payroll-runs/index.mjs.map +1 -1
- package/resources/payroll-runs/off-cycle.d.mts +322 -0
- package/resources/payroll-runs/off-cycle.d.mts.map +1 -0
- package/resources/payroll-runs/off-cycle.d.ts +322 -0
- package/resources/payroll-runs/off-cycle.d.ts.map +1 -0
- package/resources/payroll-runs/off-cycle.js +112 -0
- package/resources/payroll-runs/off-cycle.js.map +1 -0
- package/resources/payroll-runs/off-cycle.mjs +108 -0
- package/resources/payroll-runs/off-cycle.mjs.map +1 -0
- package/resources/payroll-runs/payroll-runs.d.mts +4 -0
- package/resources/payroll-runs/payroll-runs.d.mts.map +1 -1
- package/resources/payroll-runs/payroll-runs.d.ts +4 -0
- package/resources/payroll-runs/payroll-runs.d.ts.map +1 -1
- package/resources/payroll-runs/payroll-runs.js +4 -0
- package/resources/payroll-runs/payroll-runs.js.map +1 -1
- package/resources/payroll-runs/payroll-runs.mjs +4 -0
- package/resources/payroll-runs/payroll-runs.mjs.map +1 -1
- package/resources/sandboxes.d.mts +17 -11
- package/resources/sandboxes.d.mts.map +1 -1
- package/resources/sandboxes.d.ts +17 -11
- package/resources/sandboxes.d.ts.map +1 -1
- package/resources/sandboxes.js +1 -3
- package/resources/sandboxes.js.map +1 -1
- package/resources/sandboxes.mjs +1 -3
- package/resources/sandboxes.mjs.map +1 -1
- package/resources/variable-compensation-payout-labels.d.mts +3 -2
- package/resources/variable-compensation-payout-labels.d.mts.map +1 -1
- package/resources/variable-compensation-payout-labels.d.ts +3 -2
- package/resources/variable-compensation-payout-labels.d.ts.map +1 -1
- package/resources/variable-compensation-payout-labels.js +3 -2
- package/resources/variable-compensation-payout-labels.js.map +1 -1
- package/resources/variable-compensation-payout-labels.mjs +3 -2
- package/resources/variable-compensation-payout-labels.mjs.map +1 -1
- package/resources/variable-compensation-payout-types.d.mts +2 -2
- package/resources/variable-compensation-payout-types.d.ts +2 -2
- package/resources/variable-compensation-payout-types.js +2 -2
- package/resources/variable-compensation-payout-types.mjs +2 -2
- package/resources/variable-compensation-payouts.d.mts +5 -2
- package/resources/variable-compensation-payouts.d.mts.map +1 -1
- package/resources/variable-compensation-payouts.d.ts +5 -2
- package/resources/variable-compensation-payouts.d.ts.map +1 -1
- package/resources/variable-compensation-payouts.js +5 -2
- package/resources/variable-compensation-payouts.js.map +1 -1
- package/resources/variable-compensation-payouts.mjs +5 -2
- package/resources/variable-compensation-payouts.mjs.map +1 -1
- package/src/client.ts +38 -0
- package/src/lib/charts/README.md +114 -0
- package/src/lib/charts/builders.ts +90 -0
- package/src/lib/charts/describe.ts +67 -0
- package/src/lib/charts/errors.ts +48 -0
- package/src/lib/charts/evaluate.ts +114 -0
- package/src/lib/charts/guards.ts +42 -0
- package/src/lib/charts/schema.ts +72 -0
- package/src/lib/charts/types.ts +113 -0
- package/src/lib/charts.ts +43 -0
- package/src/lib/manifest/_existing-component-fields.ts +2 -0
- package/src/lib/manifest/existing-manifest-context.ts +22 -0
- package/src/lib/manifest/workflow/index.ts +64 -0
- package/src/resources/activity-log.ts +645 -0
- package/src/resources/compensations.ts +1 -1
- package/src/resources/developer-program-application.ts +1 -1
- package/src/resources/developer-users.ts +2 -1
- package/src/resources/hire-requirements.ts +8 -85
- package/src/resources/hires.ts +866 -213
- package/src/resources/index.ts +13 -0
- package/src/resources/listing-submissions.ts +1 -1
- package/src/resources/listing-transitions.ts +1 -1
- package/src/resources/listing.ts +1 -1
- package/src/resources/payroll-payment-mode-changes.ts +121 -0
- package/src/resources/payroll-runs/index.ts +1 -0
- package/src/resources/payroll-runs/off-cycle.ts +372 -0
- package/src/resources/payroll-runs/payroll-runs.ts +10 -0
- package/src/resources/sandboxes.ts +17 -11
- package/src/resources/variable-compensation-payout-labels.ts +3 -2
- package/src/resources/variable-compensation-payout-types.ts +2 -2
- package/src/resources/variable-compensation-payouts.ts +5 -2
- package/src/version.ts +1 -1
- package/version.d.mts +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.mjs +1 -1
|
@@ -7,9 +7,12 @@ import { path } from "../internal/utils/path.mjs";
|
|
|
7
7
|
*/
|
|
8
8
|
export class VariableCompensationPayouts extends APIResource {
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* List variable compensation payouts. Paginated with limit and cursor. Filters use
|
|
11
|
+
* the documented query parameters only (not OData filter). Always uses the company
|
|
12
|
+
* admin payout rowset; callers without admin payout visibility receive an empty
|
|
13
|
+
* list. Retrieve returns NotFound for the same rows.
|
|
11
14
|
*
|
|
12
|
-
* - Requires: `
|
|
15
|
+
* - Requires: `Variable Compensation Scheduled Payouts API`
|
|
13
16
|
*
|
|
14
17
|
* @example HTTP
|
|
15
18
|
* Representative request and response shapes.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"variable-compensation-payouts.mjs","sourceRoot":"","sources":["../src/resources/variable-compensation-payouts.ts"],"names":[],"mappings":"AAAA,sFAAsF;AAEtF,OAAO,EAAE,WAAW,EAAE,6BAAyB;AAG/C,OAAO,EAAE,aAAa,EAAe,+BAA2B;AAEhE,OAAO,EAAE,IAAI,EAAE,mCAA+B;AAE9C;;GAEG;AACH,MAAM,OAAO,2BAA4B,SAAQ,WAAW;IAC1D
|
|
1
|
+
{"version":3,"file":"variable-compensation-payouts.mjs","sourceRoot":"","sources":["../src/resources/variable-compensation-payouts.ts"],"names":[],"mappings":"AAAA,sFAAsF;AAEtF,OAAO,EAAE,WAAW,EAAE,6BAAyB;AAG/C,OAAO,EAAE,aAAa,EAAe,+BAA2B;AAEhE,OAAO,EAAE,IAAI,EAAE,mCAA+B;AAE9C;;GAEG;AACH,MAAM,OAAO,2BAA4B,SAAQ,WAAW;IAC1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6DG;IACH,IAAI,CACF,QAAiE,EAAE,EACnE,OAAwB;QAKxB,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAC5B,iCAAiC,EACjC,CAAA,aAAqD,CAAA,EACrD,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CACtB,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoEG;IACH,MAAM,CACJ,IAA4C,EAC5C,OAAwB;QAExB,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,iCAAiC,EAAE,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;IACpF,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,QAAQ,CAAC,EAAU,EAAE,OAAwB;QAC3C,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAA,kCAAkC,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;IAChF,CAAC;CACF"}
|
package/src/client.ts
CHANGED
|
@@ -19,6 +19,14 @@ import { AbstractPage, PageCursorURLResponse } from './core/pagination';
|
|
|
19
19
|
import * as Uploads from './core/uploads';
|
|
20
20
|
import * as API from './resources/index';
|
|
21
21
|
import { APIPromise } from './core/api-promise';
|
|
22
|
+
import {
|
|
23
|
+
ActivityLog,
|
|
24
|
+
ActivityLogListParams,
|
|
25
|
+
ActivityLogListResponse,
|
|
26
|
+
ActivityLogListResponsesPageCursorURL,
|
|
27
|
+
ActivityLogRetrieveParams,
|
|
28
|
+
ActivityLogRetrieveResponse,
|
|
29
|
+
} from './resources/activity-log';
|
|
22
30
|
import {
|
|
23
31
|
AppUserListParams,
|
|
24
32
|
AppUserListResponse,
|
|
@@ -483,6 +491,11 @@ import {
|
|
|
483
491
|
PackageVersionComponentUpdateRetrieveResponse,
|
|
484
492
|
PackageVersionComponentUpdates,
|
|
485
493
|
} from './resources/package-version-component-updates';
|
|
494
|
+
import {
|
|
495
|
+
PayrollPaymentModeChangeCreateParams,
|
|
496
|
+
PayrollPaymentModeChangeCreateResponse,
|
|
497
|
+
PayrollPaymentModeChanges,
|
|
498
|
+
} from './resources/payroll-payment-mode-changes';
|
|
486
499
|
import {
|
|
487
500
|
PlatformCapabilities,
|
|
488
501
|
PlatformCapabilityListResponse,
|
|
@@ -1682,6 +1695,10 @@ export class RipplingSDK {
|
|
|
1682
1695
|
* Job related information for the company
|
|
1683
1696
|
*/
|
|
1684
1697
|
jobPayRateExceptions: API.JobPayRateExceptions = new API.JobPayRateExceptions(this);
|
|
1698
|
+
/**
|
|
1699
|
+
* Visible Activity Log events recorded for the company
|
|
1700
|
+
*/
|
|
1701
|
+
activityLog: API.ActivityLog = new API.ActivityLog(this);
|
|
1685
1702
|
/**
|
|
1686
1703
|
* Agents defined by the company
|
|
1687
1704
|
*/
|
|
@@ -1859,6 +1876,10 @@ export class RipplingSDK {
|
|
|
1859
1876
|
packageVersionComponentUpdates: API.PackageVersionComponentUpdates = new API.PackageVersionComponentUpdates(
|
|
1860
1877
|
this,
|
|
1861
1878
|
);
|
|
1879
|
+
/**
|
|
1880
|
+
* Payroll runs for the company
|
|
1881
|
+
*/
|
|
1882
|
+
payrollPaymentModeChanges: API.PayrollPaymentModeChanges = new API.PayrollPaymentModeChanges(this);
|
|
1862
1883
|
/**
|
|
1863
1884
|
* Payroll runs for the company
|
|
1864
1885
|
*/
|
|
@@ -1990,6 +2011,7 @@ RipplingSDK.BusinessPartnerGroups = BusinessPartnerGroups;
|
|
|
1990
2011
|
RipplingSDK.Functions = Functions;
|
|
1991
2012
|
RipplingSDK.JobAssignments = JobAssignments;
|
|
1992
2013
|
RipplingSDK.JobPayRateExceptions = JobPayRateExceptions;
|
|
2014
|
+
RipplingSDK.ActivityLog = ActivityLog;
|
|
1993
2015
|
RipplingSDK.Agents = Agents;
|
|
1994
2016
|
RipplingSDK.AppUsers = AppUsers;
|
|
1995
2017
|
RipplingSDK.BackgroundCheckCandidates = BackgroundCheckCandidates;
|
|
@@ -2036,6 +2058,7 @@ RipplingSDK.PackageUninstalls = PackageUninstalls;
|
|
|
2036
2058
|
RipplingSDK.PackageUpgrades = PackageUpgrades;
|
|
2037
2059
|
RipplingSDK.PackageVersions = PackageVersions;
|
|
2038
2060
|
RipplingSDK.PackageVersionComponentUpdates = PackageVersionComponentUpdates;
|
|
2061
|
+
RipplingSDK.PayrollPaymentModeChanges = PayrollPaymentModeChanges;
|
|
2039
2062
|
RipplingSDK.PayrollRuns = PayrollRuns;
|
|
2040
2063
|
RipplingSDK.PlatformCapabilities = PlatformCapabilities;
|
|
2041
2064
|
RipplingSDK.Reports = Reports;
|
|
@@ -2369,6 +2392,15 @@ export declare namespace RipplingSDK {
|
|
|
2369
2392
|
type JobPayRateExceptionListChangeLogParams as JobPayRateExceptionListChangeLogParams,
|
|
2370
2393
|
};
|
|
2371
2394
|
|
|
2395
|
+
export {
|
|
2396
|
+
ActivityLog as ActivityLog,
|
|
2397
|
+
type ActivityLogListResponse as ActivityLogListResponse,
|
|
2398
|
+
type ActivityLogRetrieveResponse as ActivityLogRetrieveResponse,
|
|
2399
|
+
type ActivityLogListResponsesPageCursorURL as ActivityLogListResponsesPageCursorURL,
|
|
2400
|
+
type ActivityLogListParams as ActivityLogListParams,
|
|
2401
|
+
type ActivityLogRetrieveParams as ActivityLogRetrieveParams,
|
|
2402
|
+
};
|
|
2403
|
+
|
|
2372
2404
|
export {
|
|
2373
2405
|
Agents as Agents,
|
|
2374
2406
|
type AgentListResponse as AgentListResponse,
|
|
@@ -2757,6 +2789,12 @@ export declare namespace RipplingSDK {
|
|
|
2757
2789
|
type PackageVersionComponentUpdateCreateParams as PackageVersionComponentUpdateCreateParams,
|
|
2758
2790
|
};
|
|
2759
2791
|
|
|
2792
|
+
export {
|
|
2793
|
+
PayrollPaymentModeChanges as PayrollPaymentModeChanges,
|
|
2794
|
+
type PayrollPaymentModeChangeCreateResponse as PayrollPaymentModeChangeCreateResponse,
|
|
2795
|
+
type PayrollPaymentModeChangeCreateParams as PayrollPaymentModeChangeCreateParams,
|
|
2796
|
+
};
|
|
2797
|
+
|
|
2760
2798
|
export {
|
|
2761
2799
|
PayrollRuns as PayrollRuns,
|
|
2762
2800
|
type PayrollRunListResponse as PayrollRunListResponse,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Chart Rules
|
|
2
|
+
|
|
3
|
+
Handwritten helpers for authoring the _behaviour_ of an AI-generated dashboard
|
|
4
|
+
chart in a form that can be read back and edited.
|
|
5
|
+
|
|
6
|
+
A dashboard chart is drawn by a visualization function the agent writes. Some of
|
|
7
|
+
what that function decides — which colour a bar takes — is worked out fresh each
|
|
8
|
+
render from the data. Written as ordinary code, that decision vanishes into the
|
|
9
|
+
output: the chart shows _what_ colour it drew, never _why_. This library lets the
|
|
10
|
+
agent state the decision as a small rule instead, so the settings panel can show
|
|
11
|
+
it, explain it, and let a user change it.
|
|
12
|
+
|
|
13
|
+
This is not a charting library. It does not render, own an ECharts option, fetch
|
|
14
|
+
data, or touch the DOM. It produces and interprets plain data.
|
|
15
|
+
|
|
16
|
+
## Scope (v1: conditional colour)
|
|
17
|
+
|
|
18
|
+
v1 is deliberately narrow — the **colour channel only**: colour a series or datum
|
|
19
|
+
by comparing its value against a constant.
|
|
20
|
+
|
|
21
|
+
- **`value`** — this data point's plotted numeric value (a bar's height, a
|
|
22
|
+
point's y, a slice's amount). It is what the rule compares.
|
|
23
|
+
- **Comparisons** — `equals`, `isNot`, `lessThan`, `greaterThan`, `lessOrEqual`, `greaterOrEqual`.
|
|
24
|
+
- **`when(condition, ifTrue, ifFalse?)`** — a conditional value (channel-agnostic).
|
|
25
|
+
`ifFalse` is optional and defaults to `null` — for a colour, no override (default kept).
|
|
26
|
+
- **`color(rule)`** — target the colour channel. Wrapping `when(...)` in `color(...)`
|
|
27
|
+
is what says "this rule sets the colour"; the channel is separate from the condition,
|
|
28
|
+
so more channels (opacity, label visibility) are just more wrappers later.
|
|
29
|
+
- **`literal`** — bare constants (`100`, `'#E24B4A'`) are wrapped automatically.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { color, when, lessThan, greaterThan, value } from '@rippling/rippling-sdk/lib/charts';
|
|
33
|
+
|
|
34
|
+
// red when this point's value is below 100, blue otherwise
|
|
35
|
+
const barColour = color(when(lessThan(value, 100), '#E24B4A', '#378ADD'));
|
|
36
|
+
|
|
37
|
+
// highlight only the points over target; leave the rest their default colour
|
|
38
|
+
const highlight = color(when(greaterThan(value, 1000), '#E24B4A'));
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Intended flow
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
Agent writes a rule with these builders (the only consumer that imports the SDK)
|
|
45
|
+
-> color(when(lessThan(value, 100), '#E24B4A', '#378ADD')) returns a {$rule} placeholder
|
|
46
|
+
-> the placeholder is emitted into the chart option, like the {$fn} placeholders
|
|
47
|
+
a chart already emits, and crosses to the app as plain JSON
|
|
48
|
+
-> renderer resolves {$rule} against the point's value -> a colour, drawn
|
|
49
|
+
-> settings panel reads the same {$rule} tree -> controls the user can edit
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"channel": "color",
|
|
55
|
+
"$rule": {
|
|
56
|
+
"kind": "ifElse",
|
|
57
|
+
"cond": { "kind": "lessThan", "left": { "kind": "value" }, "right": { "kind": "literal", "value": 100 } },
|
|
58
|
+
"then": { "kind": "literal", "value": "#E24B4A" },
|
|
59
|
+
"else": { "kind": "literal", "value": "#378ADD" }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`channel: 'color'` says what the rule targets, so a reader sees it without
|
|
65
|
+
inferring from where it sits. `evaluate(rule, { value })` resolves it to a colour
|
|
66
|
+
for the renderer; `describe(rule)` turns it into the controls the settings panel
|
|
67
|
+
shows. Both accept the placeholder or the bare tree. A node can only ever be one
|
|
68
|
+
of a fixed set of kinds, so the three faces never drift — the `chartRuleSchema`
|
|
69
|
+
export describes that closed set.
|
|
70
|
+
|
|
71
|
+
## How it is consumed
|
|
72
|
+
|
|
73
|
+
The app never has to import this package. The rule crosses as data — a `{$rule}`
|
|
74
|
+
placeholder alongside the `{$fn}` ones — and the app's own rehydrator resolves it
|
|
75
|
+
at render time. Two ways, and only the first is needed:
|
|
76
|
+
|
|
77
|
+
1. **Contract-only** — the app keeps its own small evaluator and validates it
|
|
78
|
+
against `chartRuleSchema` (published here) with a contract test, so the two
|
|
79
|
+
can't drift without CI noticing. No dependency on this package.
|
|
80
|
+
2. **Shared** — the app imports `evaluate`/`describe` from here, so the closed
|
|
81
|
+
set is defined exactly once. A no-drift choice, not a requirement.
|
|
82
|
+
|
|
83
|
+
## Where it is going
|
|
84
|
+
|
|
85
|
+
The vocabulary starts small on purpose and is built to grow. Each addition is a
|
|
86
|
+
new kind (or channel) — one builder, one `evaluate` case, one `describe` case,
|
|
87
|
+
one `schema` entry — and it becomes editable in the panel the moment it lands,
|
|
88
|
+
with no change to charts already using it. Planned next, roughly in order:
|
|
89
|
+
|
|
90
|
+
- **More channels** — opacity, label visibility (`show`), and the like, added
|
|
91
|
+
alongside `channel: 'color'`.
|
|
92
|
+
- **Collection refs + aggregates** — `rows`, `series`, and `count`/`sum`/`avg`/`min`/`max`,
|
|
93
|
+
so a rule can compare a point against the series (e.g. the max bar, or below average).
|
|
94
|
+
- **Logic** — `and`, `or`, `not` to compose several conditions.
|
|
95
|
+
- **More refs** — `index`, `name`, `field('column')`.
|
|
96
|
+
- **`percentile`, arithmetic** (`plus`, `minus`, `mod`), **`between`, `isIn`**.
|
|
97
|
+
- **Shaping** — `filter`, `sortBy`, `limit`, `groupSmallest` (change the data before it is drawn).
|
|
98
|
+
|
|
99
|
+
## What this library does not do
|
|
100
|
+
|
|
101
|
+
- It does not render or hold an ECharts option — it returns values and controls.
|
|
102
|
+
- It has no I/O. A rule cannot reach the network, the DOM, or the clock, because
|
|
103
|
+
there is no node for it. Evaluating a rule reads only the context it is given.
|
|
104
|
+
|
|
105
|
+
## Safety
|
|
106
|
+
|
|
107
|
+
A rule is data, not code, so evaluating one is not running code. The vocabulary
|
|
108
|
+
has no primitive with a side effect, so an untrusted, user-edited tree is safe
|
|
109
|
+
to evaluate: the worst a malformed tree can do is throw a `ChartRuleError`, which
|
|
110
|
+
the caller catches and turns into the read-only "configured in code" state. There
|
|
111
|
+
are no loops or recursion, and a tree nested past `MAX_RULE_DEPTH` throws
|
|
112
|
+
`RuleDepthError` (also a `ChartRuleError`) rather than overflowing the stack.
|
|
113
|
+
Catch `ChartRuleError` — every failure mode is a subclass of it (`UnknownNodeError`,
|
|
114
|
+
`RuleTypeError`, `RuleDepthError`).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ChartRuleError } from './errors';
|
|
2
|
+
import {
|
|
3
|
+
type ComparisonKind,
|
|
4
|
+
type ComparisonNode,
|
|
5
|
+
type IfElseNode,
|
|
6
|
+
type LiteralNode,
|
|
7
|
+
type RefNode,
|
|
8
|
+
type RuleNode,
|
|
9
|
+
type RulePlaceholder,
|
|
10
|
+
type Scalar,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
/** A rule node, a nested rule (`{$rule}` placeholder), or a bare scalar. */
|
|
14
|
+
export type Operand = RuleNode | RulePlaceholder | Scalar;
|
|
15
|
+
|
|
16
|
+
function isRuleNode(operand: Operand): operand is RuleNode {
|
|
17
|
+
return typeof operand === 'object' && operand !== null && 'kind' in operand;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** True when a value is a `{$rule}` placeholder — the marker the builders emit. */
|
|
21
|
+
export function isRulePlaceholder(value: unknown): value is RulePlaceholder {
|
|
22
|
+
return typeof value === 'object' && value !== null && '$rule' in value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Wrap a bare scalar as a literal node. */
|
|
26
|
+
export function literal(value: Scalar): LiteralNode {
|
|
27
|
+
return { kind: 'literal', value };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function toNode(operand: Operand): RuleNode {
|
|
31
|
+
if (isRulePlaceholder(operand)) {
|
|
32
|
+
return operand.$rule; // a nested rule — unwrap it, never wrap it as a literal
|
|
33
|
+
}
|
|
34
|
+
return isRuleNode(operand) ? operand : literal(operand);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `value` — this data point's plotted numeric value: a bar's height, a point's
|
|
39
|
+
* y, a slice's amount. It is what a colour rule compares against a constant.
|
|
40
|
+
*/
|
|
41
|
+
export const value: RefNode = Object.freeze({ kind: 'value' });
|
|
42
|
+
|
|
43
|
+
/* Comparisons — return a condition. */
|
|
44
|
+
function comparison(kind: ComparisonKind): (left: Operand, right: Operand) => ComparisonNode {
|
|
45
|
+
return (left, right) => ({ kind, left: toNode(left), right: toNode(right) });
|
|
46
|
+
}
|
|
47
|
+
export const equals = comparison('equals');
|
|
48
|
+
export const isNot = comparison('isNot');
|
|
49
|
+
export const lessThan = comparison('lessThan');
|
|
50
|
+
export const greaterThan = comparison('greaterThan');
|
|
51
|
+
export const lessOrEqual = comparison('lessOrEqual');
|
|
52
|
+
export const greaterOrEqual = comparison('greaterOrEqual');
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A conditional value: pick `ifTrue` when the condition holds, else `ifFalse`.
|
|
56
|
+
* Channel-agnostic — wrap it in a channel builder (`color`) to target something.
|
|
57
|
+
*
|
|
58
|
+
* when(lessThan(value, 100), '#E24B4A', '#378ADD')
|
|
59
|
+
* // → '#E24B4A' when this point's value is under 100, '#378ADD' otherwise
|
|
60
|
+
*
|
|
61
|
+
* `ifFalse` is optional and defaults to `null` — for a colour, that means no
|
|
62
|
+
* override (the chart's default is kept). Returns a plain node, not a placeholder.
|
|
63
|
+
*/
|
|
64
|
+
export function when(condition: RuleNode, ifTrue: Operand, ifFalse?: Operand): IfElseNode {
|
|
65
|
+
if (ifTrue === undefined) {
|
|
66
|
+
throw new ChartRuleError('when(condition, ifTrue, ifFalse?) needs a value for the true case');
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
kind: 'ifElse',
|
|
70
|
+
cond: condition,
|
|
71
|
+
then: toNode(ifTrue),
|
|
72
|
+
else: toNode(ifFalse ?? null),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Target the colour channel — the v1 channel. Wrap a conditional value so the
|
|
78
|
+
* renderer knows it resolves to a colour, and the panel knows what it edits.
|
|
79
|
+
*
|
|
80
|
+
* color(when(lessThan(value, 100), '#E24B4A', '#378ADD'))
|
|
81
|
+
* // → red when this point's value is under 100, blue otherwise
|
|
82
|
+
*
|
|
83
|
+
* Takes a conditional value — a `when(...)` (`IfElseNode`) or an already-wrapped
|
|
84
|
+
* rule — not a bare `value` or comparison: those resolve to a number or a
|
|
85
|
+
* boolean, which is not a colour. `evaluate` enforces the same at runtime for a
|
|
86
|
+
* deserialized rule. Returns a `{$rule}` placeholder tagged `channel: 'color'`.
|
|
87
|
+
*/
|
|
88
|
+
export function color(rule: IfElseNode | RulePlaceholder): RulePlaceholder {
|
|
89
|
+
return { $rule: isRulePlaceholder(rule) ? rule.$rule : rule, channel: 'color' };
|
|
90
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isRulePlaceholder } from './builders';
|
|
2
|
+
import { RuleDepthError, UnknownNodeError } from './errors';
|
|
3
|
+
import { assertRuleNode, literalValue } from './guards';
|
|
4
|
+
import { MAX_RULE_DEPTH, type Control, type Rule, type RuleNode } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Turn a rule into the shape the settings panel renders: a label plus its
|
|
8
|
+
* editable parts. Accepts a `{$rule}` placeholder or a bare tree. Walks the same
|
|
9
|
+
* tree `evaluate` runs — and validates each node the same way — so a malformed or
|
|
10
|
+
* unknown node throws a `ChartRuleError` here too, never a native `TypeError`.
|
|
11
|
+
* The unwrap is gated on `isRulePlaceholder` (not a raw `'$rule' in rule`) so a
|
|
12
|
+
* `null`/primitive rule reaches `assertRuleNode` instead of throwing on `in`.
|
|
13
|
+
* A tree nested past `MAX_RULE_DEPTH` throws `RuleDepthError`.
|
|
14
|
+
*/
|
|
15
|
+
export function describe(rule: Rule): Control {
|
|
16
|
+
if (isRulePlaceholder(rule)) {
|
|
17
|
+
return visit(rule.$rule, 0);
|
|
18
|
+
}
|
|
19
|
+
return visit(rule, 0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function visit(node: RuleNode, depth: number): Control {
|
|
23
|
+
if (depth > MAX_RULE_DEPTH) {
|
|
24
|
+
throw new RuleDepthError(MAX_RULE_DEPTH);
|
|
25
|
+
}
|
|
26
|
+
assertRuleNode(node);
|
|
27
|
+
const next = depth + 1;
|
|
28
|
+
switch (node.kind) {
|
|
29
|
+
case 'value':
|
|
30
|
+
return { role: 'ref', label: "this row's value" };
|
|
31
|
+
case 'literal': {
|
|
32
|
+
const scalar = literalValue(node);
|
|
33
|
+
return { role: 'literal', label: String(scalar), value: scalar };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
case 'equals':
|
|
37
|
+
return comparison('is', node.left, node.right, next);
|
|
38
|
+
case 'isNot':
|
|
39
|
+
return comparison('is not', node.left, node.right, next);
|
|
40
|
+
case 'lessThan':
|
|
41
|
+
return comparison('is under', node.left, node.right, next);
|
|
42
|
+
case 'greaterThan':
|
|
43
|
+
return comparison('is over', node.left, node.right, next);
|
|
44
|
+
case 'lessOrEqual':
|
|
45
|
+
return comparison('is at most', node.left, node.right, next);
|
|
46
|
+
case 'greaterOrEqual':
|
|
47
|
+
return comparison('is at least', node.left, node.right, next);
|
|
48
|
+
|
|
49
|
+
case 'ifElse':
|
|
50
|
+
return {
|
|
51
|
+
role: 'rule',
|
|
52
|
+
label: 'when',
|
|
53
|
+
parts: [visit(node.cond, next), visit(node.then, next), visit(node.else, next)],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
default:
|
|
57
|
+
return unreachable(node);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function comparison(label: string, left: RuleNode, right: RuleNode, depth: number): Control {
|
|
62
|
+
return { role: 'comparison', label, parts: [visit(left, depth), visit(right, depth)] };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function unreachable(node: never): never {
|
|
66
|
+
throw new UnknownNodeError((node as { kind?: string }).kind);
|
|
67
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Base class for every error the chart-rule evaluator can throw. */
|
|
2
|
+
export class ChartRuleError extends Error {}
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A node whose `kind` is not in the vocabulary. The caller catches this and
|
|
6
|
+
* shows the setting as "configured in code", read-only.
|
|
7
|
+
*/
|
|
8
|
+
export class UnknownNodeError extends ChartRuleError {
|
|
9
|
+
constructor(public readonly kind?: string) {
|
|
10
|
+
super(`Unknown chart-rule node: ${kind ?? '(missing kind)'}`);
|
|
11
|
+
this.name = 'UnknownNodeError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** A node evaluated to the wrong type of value (e.g. comparing non-numbers). */
|
|
16
|
+
export class RuleTypeError extends ChartRuleError {
|
|
17
|
+
constructor(message: string) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'RuleTypeError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A structurally invalid node in a deserialized tree: not an object, no string
|
|
25
|
+
* `kind`, or a `literal` without a scalar `value`. Distinct from
|
|
26
|
+
* `UnknownNodeError` (a well-formed node whose `kind` is simply not in the
|
|
27
|
+
* vocabulary — the intentional escape hatch). Like the others it is a
|
|
28
|
+
* `ChartRuleError`, so a caller reading an untrusted rule catches it instead of
|
|
29
|
+
* a native `TypeError` (or, worse, an `undefined` slipping through silently).
|
|
30
|
+
*/
|
|
31
|
+
export class MalformedRuleError extends ChartRuleError {
|
|
32
|
+
constructor(message: string) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'MalformedRuleError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A rule nested deeper than the evaluator will walk. Guards against a
|
|
40
|
+
* stack-overflowing tree from untrusted, deserialized input; like the others it
|
|
41
|
+
* is a `ChartRuleError`, so the caller's escape hatch catches it.
|
|
42
|
+
*/
|
|
43
|
+
export class RuleDepthError extends ChartRuleError {
|
|
44
|
+
constructor(maxDepth: number) {
|
|
45
|
+
super(`chart rule nested deeper than ${maxDepth} levels`);
|
|
46
|
+
this.name = 'RuleDepthError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { isRulePlaceholder } from './builders';
|
|
2
|
+
import { MalformedRuleError, RuleDepthError, RuleTypeError, UnknownNodeError } from './errors';
|
|
3
|
+
import { assertRuleNode, literalValue } from './guards';
|
|
4
|
+
import {
|
|
5
|
+
MAX_RULE_DEPTH,
|
|
6
|
+
type Rule,
|
|
7
|
+
type RuleChannel,
|
|
8
|
+
type RuleContext,
|
|
9
|
+
type RuleNode,
|
|
10
|
+
type RuleValue,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a rule to a value against `ctx`. Accepts a `{$rule}` placeholder or a
|
|
15
|
+
* bare tree. A malformed or unknown node throws a `ChartRuleError` (never a
|
|
16
|
+
* native `TypeError`), so a caller reading an untrusted, deserialized rule can
|
|
17
|
+
* catch every failure with one type. When the rule is a colour placeholder, the
|
|
18
|
+
* result is checked to be a colour (`string | null`) — a rule that resolves to a
|
|
19
|
+
* number or boolean is a `RuleTypeError`, not something the renderer paints.
|
|
20
|
+
*
|
|
21
|
+
* Pure: the only inputs are the node and the context. No clock, no randomness,
|
|
22
|
+
* no I/O — which is what makes it safe to run on an untrusted, user-edited tree
|
|
23
|
+
* and identical across the renderer, the panel readout, and a test. A tree
|
|
24
|
+
* nested past `MAX_RULE_DEPTH` throws `RuleDepthError` rather than overflowing.
|
|
25
|
+
*/
|
|
26
|
+
export function evaluate(rule: Rule, ctx: RuleContext): RuleValue {
|
|
27
|
+
if (isRulePlaceholder(rule)) {
|
|
28
|
+
return coerceForChannel(rule.channel, visit(rule.$rule, ctx, 0));
|
|
29
|
+
}
|
|
30
|
+
return visit(rule, ctx, 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function visit(node: RuleNode, ctx: RuleContext, depth: number): RuleValue {
|
|
34
|
+
if (depth > MAX_RULE_DEPTH) {
|
|
35
|
+
throw new RuleDepthError(MAX_RULE_DEPTH);
|
|
36
|
+
}
|
|
37
|
+
assertRuleNode(node);
|
|
38
|
+
const next = depth + 1;
|
|
39
|
+
switch (node.kind) {
|
|
40
|
+
case 'value':
|
|
41
|
+
return ctx.value;
|
|
42
|
+
case 'literal':
|
|
43
|
+
return literalValue(node);
|
|
44
|
+
|
|
45
|
+
case 'equals':
|
|
46
|
+
return visit(node.left, ctx, next) === visit(node.right, ctx, next);
|
|
47
|
+
case 'isNot':
|
|
48
|
+
return visit(node.left, ctx, next) !== visit(node.right, ctx, next);
|
|
49
|
+
case 'lessThan':
|
|
50
|
+
return num(visit(node.left, ctx, next)) < num(visit(node.right, ctx, next));
|
|
51
|
+
case 'greaterThan':
|
|
52
|
+
return num(visit(node.left, ctx, next)) > num(visit(node.right, ctx, next));
|
|
53
|
+
case 'lessOrEqual':
|
|
54
|
+
return num(visit(node.left, ctx, next)) <= num(visit(node.right, ctx, next));
|
|
55
|
+
case 'greaterOrEqual':
|
|
56
|
+
return num(visit(node.left, ctx, next)) >= num(visit(node.right, ctx, next));
|
|
57
|
+
|
|
58
|
+
case 'ifElse':
|
|
59
|
+
return bool(visit(node.cond, ctx, next)) ? visit(node.then, ctx, next) : visit(node.else, ctx, next);
|
|
60
|
+
|
|
61
|
+
default:
|
|
62
|
+
return unreachable(node);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* -------------------------------------------------------------------------- */
|
|
67
|
+
/* Coercions — narrow a RuleValue, or throw a typed error. */
|
|
68
|
+
/* -------------------------------------------------------------------------- */
|
|
69
|
+
|
|
70
|
+
function num(value: RuleValue): number {
|
|
71
|
+
if (typeof value === 'number') {
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
throw new RuleTypeError(`expected a number, got ${typeName(value)}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function bool(value: RuleValue): boolean {
|
|
78
|
+
if (typeof value === 'boolean') {
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
throw new RuleTypeError(`expected a boolean, got ${typeName(value)}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A colour channel must resolve to a colour string, or null for "no override". */
|
|
85
|
+
function asColor(value: RuleValue): RuleValue {
|
|
86
|
+
if (value === null || typeof value === 'string') {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
throw new RuleTypeError(`a colour rule must resolve to a colour string or null, got ${typeName(value)}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Coerce a resolved value to what its channel can use. A colour rule must
|
|
94
|
+
* resolve to a colour; a future channel would add its own case here. A
|
|
95
|
+
* placeholder whose `channel` is missing or unrecognised (only possible from
|
|
96
|
+
* untrusted, deserialized input — the type and schema both pin it to `'color'`)
|
|
97
|
+
* is malformed rather than silently unchecked.
|
|
98
|
+
*/
|
|
99
|
+
function coerceForChannel(channel: RuleChannel, value: RuleValue): RuleValue {
|
|
100
|
+
switch (channel) {
|
|
101
|
+
case 'color':
|
|
102
|
+
return asColor(value);
|
|
103
|
+
default:
|
|
104
|
+
throw new MalformedRuleError(`a rule placeholder has an unsupported channel: ${String(channel)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function typeName(value: RuleValue): string {
|
|
109
|
+
return value === null ? 'null' : typeof value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function unreachable(node: never): never {
|
|
113
|
+
throw new UnknownNodeError((node as { kind?: string }).kind);
|
|
114
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { MalformedRuleError } from './errors';
|
|
2
|
+
import { type RuleNode, type Scalar } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runtime validation for one node of a deserialized, untrusted rule tree, shared
|
|
6
|
+
* by `evaluate` and `describe`. A rule can arrive from JSON that TypeScript never
|
|
7
|
+
* checked, so `null`, a non-object, or a node without a string `kind` would throw
|
|
8
|
+
* a native `TypeError` on the first field read — skipping the `ChartRuleError`
|
|
9
|
+
* escape hatch a caller relies on. Validate before reading fields.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const SCALAR_TYPES: ReadonlySet<string> = new Set(['number', 'string', 'boolean']);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Assert `node` is shaped like a rule node — an object carrying a string `kind`.
|
|
16
|
+
* An unrecognised `kind` is *not* rejected here; that is the caller's `switch`
|
|
17
|
+
* default (`UnknownNodeError`), the deliberate "configured in code" signal.
|
|
18
|
+
*/
|
|
19
|
+
export function assertRuleNode(node: unknown): asserts node is RuleNode {
|
|
20
|
+
if (
|
|
21
|
+
node === null ||
|
|
22
|
+
typeof node !== 'object' ||
|
|
23
|
+
Array.isArray(node) ||
|
|
24
|
+
typeof (node as { kind?: unknown }).kind !== 'string'
|
|
25
|
+
) {
|
|
26
|
+
throw new MalformedRuleError('a rule node must be an object with a string "kind"');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read a `literal` node's `value`, rejecting a missing or non-scalar one. Without
|
|
32
|
+
* this, `{ kind: 'literal' }` resolves to `undefined` and slips past the caller.
|
|
33
|
+
*/
|
|
34
|
+
export function literalValue(node: { value?: unknown }): Scalar {
|
|
35
|
+
const { value } = node;
|
|
36
|
+
if (value === null || SCALAR_TYPES.has(typeof value)) {
|
|
37
|
+
return value as Scalar;
|
|
38
|
+
}
|
|
39
|
+
throw new MalformedRuleError(
|
|
40
|
+
`a literal must carry a scalar value, got ${value === undefined ? 'undefined' : typeof value}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CHART_RULE_CONTRACT_VERSION } from './types';
|
|
2
|
+
|
|
3
|
+
/** Stable identifier for the published contract, versioned by the tag. */
|
|
4
|
+
export const CHART_RULE_SCHEMA_ID = `https://rippling.com/schemas/${CHART_RULE_CONTRACT_VERSION}.json`;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* JSON Schema (2020-12) for the `{$rule}` placeholder and its closed node set.
|
|
8
|
+
*
|
|
9
|
+
* This is the contract. A consumer that keeps its own rule evaluator (rather
|
|
10
|
+
* than importing `evaluate`) can validate against this so the two never drift —
|
|
11
|
+
* every kind and operand shape the builders can emit is described here.
|
|
12
|
+
*/
|
|
13
|
+
export const chartRuleSchema = {
|
|
14
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
15
|
+
$id: CHART_RULE_SCHEMA_ID,
|
|
16
|
+
title: 'Chart rule placeholder',
|
|
17
|
+
description: 'A rule tree the chart agent emits into a chart option, marked with $rule.',
|
|
18
|
+
type: 'object',
|
|
19
|
+
required: ['$rule', 'channel'],
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {
|
|
22
|
+
// A colour rule's top-level tree is the conditional-value form (`when(...)`),
|
|
23
|
+
// matching what `color()` builds and accepts. A bare ref/comparison/literal
|
|
24
|
+
// resolves to a number/boolean/constant, not a colour decision.
|
|
25
|
+
$rule: { $ref: '#/$defs/ifElse' },
|
|
26
|
+
channel: { const: 'color' },
|
|
27
|
+
},
|
|
28
|
+
$defs: {
|
|
29
|
+
scalar: { type: ['number', 'string', 'boolean', 'null'] },
|
|
30
|
+
node: {
|
|
31
|
+
oneOf: [
|
|
32
|
+
{ $ref: '#/$defs/ref' },
|
|
33
|
+
{ $ref: '#/$defs/literal' },
|
|
34
|
+
{ $ref: '#/$defs/comparison' },
|
|
35
|
+
{ $ref: '#/$defs/ifElse' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
ref: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
required: ['kind'],
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
properties: { kind: { const: 'value' } },
|
|
43
|
+
},
|
|
44
|
+
literal: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
required: ['kind', 'value'],
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
properties: { kind: { const: 'literal' }, value: { $ref: '#/$defs/scalar' } },
|
|
49
|
+
},
|
|
50
|
+
comparison: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
required: ['kind', 'left', 'right'],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
properties: {
|
|
55
|
+
kind: { enum: ['equals', 'isNot', 'lessThan', 'greaterThan', 'lessOrEqual', 'greaterOrEqual'] },
|
|
56
|
+
left: { $ref: '#/$defs/node' },
|
|
57
|
+
right: { $ref: '#/$defs/node' },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
ifElse: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
required: ['kind', 'cond', 'then', 'else'],
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
properties: {
|
|
65
|
+
kind: { const: 'ifElse' },
|
|
66
|
+
cond: { $ref: '#/$defs/node' },
|
|
67
|
+
then: { $ref: '#/$defs/node' },
|
|
68
|
+
else: { $ref: '#/$defs/node' },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
} as const;
|