@reso-standards/reso-validation 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.
- package/LICENSE +11 -0
- package/README.md +109 -0
- package/dist/business-rules/index.d.ts +15 -0
- package/dist/business-rules/index.d.ts.map +1 -0
- package/dist/business-rules/index.js +87 -0
- package/dist/business-rules/index.js.map +1 -0
- package/dist/business-rules/member-rules.d.ts +4 -0
- package/dist/business-rules/member-rules.d.ts.map +1 -0
- package/dist/business-rules/member-rules.js +9 -0
- package/dist/business-rules/member-rules.js.map +1 -0
- package/dist/business-rules/office-rules.d.ts +4 -0
- package/dist/business-rules/office-rules.d.ts.map +1 -0
- package/dist/business-rules/office-rules.js +9 -0
- package/dist/business-rules/office-rules.js.map +1 -0
- package/dist/business-rules/property-rules.d.ts +6 -0
- package/dist/business-rules/property-rules.d.ts.map +1 -0
- package/dist/business-rules/property-rules.js +97 -0
- package/dist/business-rules/property-rules.js.map +1 -0
- package/dist/business-rules/types.d.ts +18 -0
- package/dist/business-rules/types.d.ts.map +1 -0
- package/dist/business-rules/types.js +2 -0
- package/dist/business-rules/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/metadata/helpers.d.ts +7 -0
- package/dist/metadata/helpers.d.ts.map +1 -0
- package/dist/metadata/helpers.js +9 -0
- package/dist/metadata/helpers.js.map +1 -0
- package/dist/metadata/index.d.ts +6 -0
- package/dist/metadata/index.d.ts.map +1 -0
- package/dist/metadata/index.js +4 -0
- package/dist/metadata/index.js.map +1 -0
- package/dist/metadata/types.d.ts +24 -0
- package/dist/metadata/types.d.ts.map +1 -0
- package/dist/metadata/types.js +2 -0
- package/dist/metadata/types.js.map +1 -0
- package/dist/metadata/validate.d.ts +16 -0
- package/dist/metadata/validate.d.ts.map +1 -0
- package/dist/metadata/validate.js +121 -0
- package/dist/metadata/validate.js.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
REAL ESTATE STANDARDS ORGANIZATION
|
|
2
|
+
End-user License Agreement (EULA)
|
|
3
|
+
|
|
4
|
+
By downloading these resources, you confirm that you agree to the RESO EULA: https://www.reso.org/eula/
|
|
5
|
+
|
|
6
|
+
Contact Information:
|
|
7
|
+
Attn: Sam DeBord, Chief Executive Officer
|
|
8
|
+
Address: P.O. Box 10824
|
|
9
|
+
Raleigh, NC 27605
|
|
10
|
+
Phone: 206.658.3225
|
|
11
|
+
Email: sam@reso.org
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @reso-standards/validation
|
|
2
|
+
|
|
3
|
+
Isomorphic TypeScript validation library for RESO Data Dictionary records. Validates field types, lengths, and ranges against metadata, plus resource-specific business rules with cross-field constraints. Zero external dependencies.
|
|
4
|
+
|
|
5
|
+
> **[User Guide](doc/GUIDE.md)** – a task-oriented walkthrough with realistic examples.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
This package isn't on npm yet. Install from the [`reso-tools`](https://github.com/RESOStandards/reso-tools) monorepo on GitHub:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
git clone https://github.com/RESOStandards/reso-tools.git
|
|
13
|
+
cd reso-tools/reso-validation
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This package has no sibling dependencies — `npm install` just runs. To bootstrap every package at once, run `npm run bootstrap` from the repo root instead.
|
|
18
|
+
|
|
19
|
+
To consume the SDK from another project, link it locally with `npm link` or use a `file:` dependency.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { validateRecord, validateBusinessRules, getBusinessRules } from '@reso-standards/validation';
|
|
25
|
+
import type { ResoField, ValidationFailure } from '@reso-standards/validation';
|
|
26
|
+
|
|
27
|
+
// Validate a record against field metadata
|
|
28
|
+
const failures = validateRecord(record, fields);
|
|
29
|
+
|
|
30
|
+
// Validate resource-specific business rules
|
|
31
|
+
const ruleFailures = validateBusinessRules('Property', record);
|
|
32
|
+
|
|
33
|
+
// Get the rule definitions for a resource
|
|
34
|
+
const rules = getBusinessRules('Property');
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### `validateRecord(body, fields): ValidationFailure[]`
|
|
40
|
+
|
|
41
|
+
Validates a record payload against RESO field metadata. Checks:
|
|
42
|
+
|
|
43
|
+
- **Unknown fields** – fields not in metadata
|
|
44
|
+
- **Type mismatches** – Edm.String, Edm.Boolean, Edm.Int32, Edm.Decimal, Edm.Date, Edm.DateTimeOffset, etc.
|
|
45
|
+
- **Negative numerics** – rejects negative values for numeric types
|
|
46
|
+
- **MaxLength** – strings exceeding `field.maxLength`
|
|
47
|
+
- **Integer enforcement** – Int types must be whole numbers
|
|
48
|
+
- **Collection fields** – must be arrays
|
|
49
|
+
- **Enum fields** – must be string or number
|
|
50
|
+
- **Business rules** – delegates to `validateBusinessRules()` for range/relationship checks
|
|
51
|
+
|
|
52
|
+
Null, undefined, and empty string values are silently skipped unless the field is required. Fields starting with `@` (OData annotations) are ignored.
|
|
53
|
+
|
|
54
|
+
### `validateBusinessRules(resourceName, body): ValidationFailure[]`
|
|
55
|
+
|
|
56
|
+
Validates resource-specific constraints:
|
|
57
|
+
|
|
58
|
+
**Property:**
|
|
59
|
+
- Required: City, StateOrProvince, PostalCode, Country
|
|
60
|
+
- Price fields (> 0 to $1B): ListPrice, OriginalListPrice, PreviousListPrice, ClosePrice, ListPriceLow
|
|
61
|
+
- Room counts (0 to 100): BedroomsTotal, BathroomsFull, BathroomsHalf, etc.
|
|
62
|
+
- Expense/fee/amount fields ($0 to $10K): matched via `fieldPattern` regex `/(?:Expense|Amount|Fee\d?)$/`
|
|
63
|
+
- Latitude and Longitude are exempt from the non-negative rule (negative coordinates are valid)
|
|
64
|
+
- Cross-field: `BathroomsTotalInteger = sum(BathroomsFull + BathroomsHalf + ...)`
|
|
65
|
+
|
|
66
|
+
**Member:** Required MemberCity, MemberStateOrProvince, MemberPostalCode, MemberCountry
|
|
67
|
+
|
|
68
|
+
**Office:** Required OfficeCity, OfficeStateOrProvince, OfficePostalCode, OfficeCountry
|
|
69
|
+
|
|
70
|
+
### `getBusinessRules(resourceName): FieldRule[]`
|
|
71
|
+
|
|
72
|
+
Returns the per-field constraint definitions for a resource.
|
|
73
|
+
|
|
74
|
+
### Type Helpers
|
|
75
|
+
|
|
76
|
+
- `isEnumType(type)` – true if type is an enum reference (not a primitive Edm type)
|
|
77
|
+
- `isNumericEdmType(type)` – true for Edm.Decimal, Edm.Int32, Edm.Double, etc.
|
|
78
|
+
- `isIntegerEdmType(type)` – true for Edm.Int16, Edm.Int32, Edm.Int64, Edm.Byte
|
|
79
|
+
|
|
80
|
+
## Types
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
interface ValidationFailure {
|
|
84
|
+
readonly field: string;
|
|
85
|
+
readonly reason: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface FieldRule {
|
|
89
|
+
readonly fieldName: string;
|
|
90
|
+
readonly fieldPattern?: RegExp; // regex alternative to fieldName for matching multiple fields
|
|
91
|
+
readonly required?: boolean;
|
|
92
|
+
readonly min?: number;
|
|
93
|
+
readonly max?: number;
|
|
94
|
+
readonly message?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface CrossFieldRule {
|
|
98
|
+
readonly name: string;
|
|
99
|
+
readonly validate: (body: Record<string, unknown>) => ValidationFailure | null;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Integration
|
|
104
|
+
|
|
105
|
+
Used by both the reference server (request body validation on POST/PATCH) and the React UI (client-side form validation). Also used by `@reso-standards/certification` for payload validation in compliance testing.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
See [LICENSE](https://github.com/RESOStandards/reso-tools/blob/main/LICENSE) in the repository root.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ValidationFailure } from '../metadata/types.js';
|
|
2
|
+
import type { CrossFieldRule, FieldRule } from './types.js';
|
|
3
|
+
export type { CrossFieldRule, FieldRule } from './types.js';
|
|
4
|
+
/** Returns the per-field business rules for a given resource (empty array if none). */
|
|
5
|
+
export declare const getBusinessRules: (resourceName: string) => ReadonlyArray<FieldRule>;
|
|
6
|
+
/** Returns the cross-field business rules for a given resource (empty array if none). */
|
|
7
|
+
export declare const getCrossFieldRules: (resourceName: string) => ReadonlyArray<CrossFieldRule>;
|
|
8
|
+
/**
|
|
9
|
+
* Validates a record against business rules for the given resource.
|
|
10
|
+
* Checks per-field range rules first, then cross-field relationship rules.
|
|
11
|
+
* When `skipRequired` is true (e.g., for PATCH partial updates), required-field checks are skipped.
|
|
12
|
+
* Returns an array of failures (empty if valid).
|
|
13
|
+
*/
|
|
14
|
+
export declare const validateBusinessRules: (resourceName: string, body: Readonly<Record<string, unknown>>, skipRequired?: boolean) => ReadonlyArray<ValidationFailure>;
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/business-rules/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAI9D,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5D,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAc5D,uFAAuF;AACvF,eAAO,MAAM,gBAAgB,GAAI,cAAc,MAAM,KAAG,aAAa,CAAC,SAAS,CAA0C,CAAC;AAE1H,yFAAyF;AACzF,eAAO,MAAM,kBAAkB,GAAI,cAAc,MAAM,KAAG,aAAa,CAAC,cAAc,CAAgD,CAAC;AAevI;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,cAAc,MAAM,EACpB,MAAM,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACvC,sBAAoB,KACnB,aAAa,CAAC,iBAAiB,CA+CjC,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { MEMBER_RULES } from './member-rules.js';
|
|
2
|
+
import { OFFICE_RULES } from './office-rules.js';
|
|
3
|
+
import { PROPERTY_CROSS_RULES, PROPERTY_RULES } from './property-rules.js';
|
|
4
|
+
/** Per-field rule registry keyed by resource name. */
|
|
5
|
+
const RULES_BY_RESOURCE = {
|
|
6
|
+
Property: PROPERTY_RULES,
|
|
7
|
+
Member: MEMBER_RULES,
|
|
8
|
+
Office: OFFICE_RULES
|
|
9
|
+
};
|
|
10
|
+
/** Cross-field rule registry keyed by resource name. */
|
|
11
|
+
const CROSS_RULES_BY_RESOURCE = {
|
|
12
|
+
Property: PROPERTY_CROSS_RULES
|
|
13
|
+
};
|
|
14
|
+
/** Returns the per-field business rules for a given resource (empty array if none). */
|
|
15
|
+
export const getBusinessRules = (resourceName) => RULES_BY_RESOURCE[resourceName] ?? [];
|
|
16
|
+
/** Returns the cross-field business rules for a given resource (empty array if none). */
|
|
17
|
+
export const getCrossFieldRules = (resourceName) => CROSS_RULES_BY_RESOURCE[resourceName] ?? [];
|
|
18
|
+
/** Formats a default error message for a range violation. */
|
|
19
|
+
const formatRangeMessage = (rule, value) => {
|
|
20
|
+
if (rule.message)
|
|
21
|
+
return rule.message;
|
|
22
|
+
if (rule.min !== undefined && value < rule.min) {
|
|
23
|
+
return `Must be at least ${rule.min.toLocaleString('en-US')}.`;
|
|
24
|
+
}
|
|
25
|
+
if (rule.max !== undefined && value > rule.max) {
|
|
26
|
+
return `Must be at most ${rule.max.toLocaleString('en-US')}.`;
|
|
27
|
+
}
|
|
28
|
+
/* v8 ignore next 2 */
|
|
29
|
+
return 'Value is out of range.';
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Validates a record against business rules for the given resource.
|
|
33
|
+
* Checks per-field range rules first, then cross-field relationship rules.
|
|
34
|
+
* When `skipRequired` is true (e.g., for PATCH partial updates), required-field checks are skipped.
|
|
35
|
+
* Returns an array of failures (empty if valid).
|
|
36
|
+
*/
|
|
37
|
+
export const validateBusinessRules = (resourceName, body, skipRequired = false) => {
|
|
38
|
+
const failures = [];
|
|
39
|
+
// Per-field rules
|
|
40
|
+
const rules = getBusinessRules(resourceName);
|
|
41
|
+
if (rules.length > 0) {
|
|
42
|
+
const exactRules = new Map();
|
|
43
|
+
const patternRules = [];
|
|
44
|
+
for (const r of rules) {
|
|
45
|
+
if (r.fieldPattern)
|
|
46
|
+
patternRules.push(r);
|
|
47
|
+
else
|
|
48
|
+
exactRules.set(r.fieldName, r);
|
|
49
|
+
}
|
|
50
|
+
// Required field checks — iterate rules looking for missing fields
|
|
51
|
+
// Skipped for partial updates (PATCH) where only supplied fields are validated
|
|
52
|
+
if (!skipRequired) {
|
|
53
|
+
for (const rule of rules) {
|
|
54
|
+
if (!rule.required)
|
|
55
|
+
continue;
|
|
56
|
+
const value = body[rule.fieldName];
|
|
57
|
+
if (value === undefined || value === null || value === '') {
|
|
58
|
+
failures.push({ field: rule.fieldName, reason: rule.message ?? `${rule.fieldName} is required.` });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Range checks — iterate body fields looking for out-of-range values
|
|
63
|
+
for (const [key, value] of Object.entries(body)) {
|
|
64
|
+
if (typeof value !== 'number')
|
|
65
|
+
continue;
|
|
66
|
+
// Check exact match first, then pattern rules
|
|
67
|
+
const rule = exactRules.get(key) ?? patternRules.find(r => r.fieldPattern.test(key));
|
|
68
|
+
if (!rule)
|
|
69
|
+
continue;
|
|
70
|
+
if (rule.min !== undefined && value < rule.min) {
|
|
71
|
+
failures.push({ field: key, reason: formatRangeMessage(rule, value) });
|
|
72
|
+
}
|
|
73
|
+
else if (rule.max !== undefined && value > rule.max) {
|
|
74
|
+
failures.push({ field: key, reason: formatRangeMessage(rule, value) });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Cross-field relationship rules
|
|
79
|
+
const crossRules = getCrossFieldRules(resourceName);
|
|
80
|
+
for (const crossRule of crossRules) {
|
|
81
|
+
const failure = crossRule.validate(body);
|
|
82
|
+
if (failure)
|
|
83
|
+
failures.push(failure);
|
|
84
|
+
}
|
|
85
|
+
return failures;
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/business-rules/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAK3E,sDAAsD;AACtD,MAAM,iBAAiB,GAAuD;IAC5E,QAAQ,EAAE,cAAc;IACxB,MAAM,EAAE,YAAY;IACpB,MAAM,EAAE,YAAY;CACrB,CAAC;AAEF,wDAAwD;AACxD,MAAM,uBAAuB,GAA4D;IACvF,QAAQ,EAAE,oBAAoB;CAC/B,CAAC;AAEF,uFAAuF;AACvF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,YAAoB,EAA4B,EAAE,CAAC,iBAAiB,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;AAE1H,yFAAyF;AACzF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,YAAoB,EAAiC,EAAE,CAAC,uBAAuB,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;AAEvI,6DAA6D;AAC7D,MAAM,kBAAkB,GAAG,CAAC,IAAe,EAAE,KAAa,EAAU,EAAE;IACpE,IAAI,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC,OAAO,CAAC;IACtC,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC/C,OAAO,oBAAoB,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC;IACjE,CAAC;IACD,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC/C,OAAO,mBAAmB,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC;IAChE,CAAC;IACD,sBAAsB;IACtB,OAAO,wBAAwB,CAAC;AAClC,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACnC,YAAoB,EACpB,IAAuC,EACvC,YAAY,GAAG,KAAK,EACc,EAAE;IACpC,MAAM,QAAQ,GAAwB,EAAE,CAAC;IAEzC,kBAAkB;IAClB,MAAM,KAAK,GAAG,gBAAgB,CAAC,YAAY,CAAC,CAAC;IAC7C,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,UAAU,GAAG,IAAI,GAAG,EAAqB,CAAC;QAChD,MAAM,YAAY,GAAgB,EAAE,CAAC;QACrC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,IAAI,CAAC,CAAC,YAAY;gBAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;;gBACpC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;QACtC,CAAC;QAED,mEAAmE;QACnE,+EAA+E;QAC/E,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,CAAC,IAAI,CAAC,QAAQ;oBAAE,SAAS;gBAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACnC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;oBAC1D,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,IAAI,GAAG,IAAI,CAAC,SAAS,eAAe,EAAE,CAAC,CAAC;gBACrG,CAAC;YACH,CAAC;QACH,CAAC;QAED,qEAAqE;QACrE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAChD,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,SAAS;YACxC,8CAA8C;YAC9C,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YACtF,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC/C,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,kBAAkB,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;YACzE,CAAC;iBAAM,IAAI,IAAI,CAAC,GAAG,KAAK,SAAS,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBACtD,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,kBAAkB,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;YACzE,CAAC;QACH,CAAC;IACH,CAAC;IAED,iCAAiC;IACjC,MAAM,UAAU,GAAG,kBAAkB,CAAC,YAAY,CAAC,CAAC;IACpD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,OAAO;YAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACtC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"member-rules.d.ts","sourceRoot":"","sources":["../../src/business-rules/member-rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,+CAA+C;AAC/C,eAAO,MAAM,YAAY,EAAE,aAAa,CAAC,SAAS,CAMjD,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Per-field rules for the Member resource. */
|
|
2
|
+
export const MEMBER_RULES = [
|
|
3
|
+
// Required address fields
|
|
4
|
+
{ fieldName: 'MemberCity', required: true },
|
|
5
|
+
{ fieldName: 'MemberStateOrProvince', required: true },
|
|
6
|
+
{ fieldName: 'MemberPostalCode', required: true },
|
|
7
|
+
{ fieldName: 'MemberCountry', required: true }
|
|
8
|
+
];
|
|
9
|
+
//# sourceMappingURL=member-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"member-rules.js","sourceRoot":"","sources":["../../src/business-rules/member-rules.ts"],"names":[],"mappings":"AAEA,+CAA+C;AAC/C,MAAM,CAAC,MAAM,YAAY,GAA6B;IACpD,0BAA0B;IAC1B,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,EAAE,SAAS,EAAE,uBAAuB,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtD,EAAE,SAAS,EAAE,kBAAkB,EAAE,QAAQ,EAAE,IAAI,EAAE;IACjD,EAAE,SAAS,EAAE,eAAe,EAAE,QAAQ,EAAE,IAAI,EAAE;CAC/C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"office-rules.d.ts","sourceRoot":"","sources":["../../src/business-rules/office-rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,+CAA+C;AAC/C,eAAO,MAAM,YAAY,EAAE,aAAa,CAAC,SAAS,CAMjD,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Per-field rules for the Office resource. */
|
|
2
|
+
export const OFFICE_RULES = [
|
|
3
|
+
// Required address fields
|
|
4
|
+
{ fieldName: 'OfficeCity', required: true },
|
|
5
|
+
{ fieldName: 'OfficeStateOrProvince', required: true },
|
|
6
|
+
{ fieldName: 'OfficePostalCode', required: true },
|
|
7
|
+
{ fieldName: 'OfficeCountry', required: true }
|
|
8
|
+
];
|
|
9
|
+
//# sourceMappingURL=office-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"office-rules.js","sourceRoot":"","sources":["../../src/business-rules/office-rules.ts"],"names":[],"mappings":"AAEA,+CAA+C;AAC/C,MAAM,CAAC,MAAM,YAAY,GAA6B;IACpD,0BAA0B;IAC1B,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE;IAC3C,EAAE,SAAS,EAAE,uBAAuB,EAAE,QAAQ,EAAE,IAAI,EAAE;IACtD,EAAE,SAAS,EAAE,kBAAkB,EAAE,QAAQ,EAAE,IAAI,EAAE;IACjD,EAAE,SAAS,EAAE,eAAe,EAAE,QAAQ,EAAE,IAAI,EAAE;CAC/C,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CrossFieldRule, FieldRule } from './types.js';
|
|
2
|
+
/** Per-field rules for the Property resource. */
|
|
3
|
+
export declare const PROPERTY_RULES: ReadonlyArray<FieldRule>;
|
|
4
|
+
/** Cross-field rules for the Property resource. */
|
|
5
|
+
export declare const PROPERTY_CROSS_RULES: ReadonlyArray<CrossFieldRule>;
|
|
6
|
+
//# sourceMappingURL=property-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"property-rules.d.ts","sourceRoot":"","sources":["../../src/business-rules/property-rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAmC5D,iDAAiD;AACjD,eAAO,MAAM,cAAc,EAAE,aAAa,CAAC,SAAS,CAmCnD,CAAC;AAaF,mDAAmD;AACnD,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,cAAc,CA2B9D,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Maximum allowed value for price fields. */
|
|
2
|
+
const MAX_PRICE = 1_000_000_000;
|
|
3
|
+
/** Maximum allowed value for bedroom/bathroom count fields. */
|
|
4
|
+
const MAX_ROOM_COUNT = 100;
|
|
5
|
+
/** Maximum allowed value for expense, fee, and amount fields. */
|
|
6
|
+
const MAX_EXPENSE = 10_000;
|
|
7
|
+
const priceRule = (fieldName) => ({
|
|
8
|
+
fieldName,
|
|
9
|
+
min: 0.01,
|
|
10
|
+
max: MAX_PRICE
|
|
11
|
+
});
|
|
12
|
+
const roomCountRule = (fieldName) => ({
|
|
13
|
+
fieldName,
|
|
14
|
+
min: 0,
|
|
15
|
+
max: MAX_ROOM_COUNT
|
|
16
|
+
});
|
|
17
|
+
const requiredRule = (fieldName) => ({
|
|
18
|
+
fieldName,
|
|
19
|
+
required: true
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Pattern matching expense, fee, and amount fields by name suffix.
|
|
23
|
+
* Matches: CableTvExpense, AssociationFee, AssociationFee2, TaxAnnualAmount, etc.
|
|
24
|
+
* Does NOT match: LotSizeSquareFeet (ends with "Feet", not "Fee").
|
|
25
|
+
*/
|
|
26
|
+
const EXPENSE_FEE_AMOUNT_PATTERN = /(?:Expense|Amount|Fee\d?)$/;
|
|
27
|
+
/** Per-field rules for the Property resource. */
|
|
28
|
+
export const PROPERTY_RULES = [
|
|
29
|
+
// Required address fields
|
|
30
|
+
requiredRule('City'),
|
|
31
|
+
requiredRule('StateOrProvince'),
|
|
32
|
+
requiredRule('PostalCode'),
|
|
33
|
+
requiredRule('Country'),
|
|
34
|
+
// Price fields (must be > $0)
|
|
35
|
+
priceRule('ListPrice'),
|
|
36
|
+
priceRule('OriginalListPrice'),
|
|
37
|
+
priceRule('PreviousListPrice'),
|
|
38
|
+
priceRule('ClosePrice'),
|
|
39
|
+
priceRule('ListPriceLow'),
|
|
40
|
+
// Expense, fee, and amount fields (pattern-matched)
|
|
41
|
+
{
|
|
42
|
+
fieldName: 'expenses/fees/amounts',
|
|
43
|
+
fieldPattern: EXPENSE_FEE_AMOUNT_PATTERN,
|
|
44
|
+
min: 0,
|
|
45
|
+
max: MAX_EXPENSE
|
|
46
|
+
},
|
|
47
|
+
// Bedroom fields
|
|
48
|
+
roomCountRule('BedroomsTotal'),
|
|
49
|
+
roomCountRule('BedroomsPossible'),
|
|
50
|
+
roomCountRule('MainLevelBedrooms'),
|
|
51
|
+
// Bathroom fields
|
|
52
|
+
roomCountRule('BathroomsTotalInteger'),
|
|
53
|
+
roomCountRule('BathroomsFull'),
|
|
54
|
+
roomCountRule('BathroomsHalf'),
|
|
55
|
+
roomCountRule('BathroomsOneQuarter'),
|
|
56
|
+
roomCountRule('BathroomsPartial'),
|
|
57
|
+
roomCountRule('BathroomsThreeQuarter'),
|
|
58
|
+
roomCountRule('MainLevelBathrooms')
|
|
59
|
+
];
|
|
60
|
+
// --- Cross-field helpers ---
|
|
61
|
+
/** Extracts a numeric value from a record field, or returns null if absent/non-numeric. */
|
|
62
|
+
const numericField = (body, field) => {
|
|
63
|
+
const v = body[field];
|
|
64
|
+
return typeof v === 'number' ? v : null;
|
|
65
|
+
};
|
|
66
|
+
/** Bathroom part fields that should sum to BathroomsTotalInteger. */
|
|
67
|
+
const BATHROOM_PARTS = ['BathroomsFull', 'BathroomsHalf', 'BathroomsPartial', 'BathroomsOneQuarter', 'BathroomsThreeQuarter'];
|
|
68
|
+
/** Cross-field rules for the Property resource. */
|
|
69
|
+
export const PROPERTY_CROSS_RULES = [
|
|
70
|
+
{
|
|
71
|
+
name: 'BathroomsTotalInteger = sum of parts',
|
|
72
|
+
validate: body => {
|
|
73
|
+
const total = numericField(body, 'BathroomsTotalInteger');
|
|
74
|
+
if (total === null)
|
|
75
|
+
return null;
|
|
76
|
+
let sum = 0;
|
|
77
|
+
let hasParts = false;
|
|
78
|
+
for (const part of BATHROOM_PARTS) {
|
|
79
|
+
const v = numericField(body, part);
|
|
80
|
+
if (v !== null) {
|
|
81
|
+
sum += v;
|
|
82
|
+
hasParts = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!hasParts)
|
|
86
|
+
return null;
|
|
87
|
+
if (total !== sum) {
|
|
88
|
+
return {
|
|
89
|
+
field: 'BathroomsTotalInteger',
|
|
90
|
+
reason: `BathroomsTotalInteger (${total}) must equal the sum of bathroom parts (${sum}).`
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
//# sourceMappingURL=property-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"property-rules.js","sourceRoot":"","sources":["../../src/business-rules/property-rules.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ValidationFailure } from '../metadata/types.js';
|
|
2
|
+
/** A per-field constraint for a specific field within a resource. */
|
|
3
|
+
export interface FieldRule {
|
|
4
|
+
readonly fieldName: string;
|
|
5
|
+
/** Pattern to match multiple field names. When set, fieldName is a descriptive label. */
|
|
6
|
+
readonly fieldPattern?: RegExp;
|
|
7
|
+
readonly required?: boolean;
|
|
8
|
+
readonly min?: number;
|
|
9
|
+
readonly max?: number;
|
|
10
|
+
/** Custom error message. If omitted, a default is generated from min/max or required. */
|
|
11
|
+
readonly message?: string;
|
|
12
|
+
}
|
|
13
|
+
/** A cross-field constraint that validates relationships between multiple fields. */
|
|
14
|
+
export interface CrossFieldRule {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly validate: (body: Readonly<Record<string, unknown>>) => ValidationFailure | null;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/business-rules/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,qEAAqE;AACrE,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KAAK,iBAAiB,GAAG,IAAI,CAAC;CAC1F"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/business-rules/types.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { CrossFieldRule, FieldRule, ResoAnnotation, ResoField, ValidationFailure } from './metadata/index.js';
|
|
2
|
+
export { getBusinessRules, isEnumType, isIntegerEdmType, isNumericEdmType, validateBusinessRules, validateRecord } from './metadata/index.js';
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,cAAc,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACnH,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACf,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACf,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Checks whether a field type is an enum reference (not a primitive Edm type). */
|
|
2
|
+
export declare const isEnumType: (type: string) => boolean;
|
|
3
|
+
/** Checks if an Edm type is numeric. */
|
|
4
|
+
export declare const isNumericEdmType: (type: string) => boolean;
|
|
5
|
+
/** Checks if an Edm type is an integer (no decimals allowed). */
|
|
6
|
+
export declare const isIntegerEdmType: (type: string) => boolean;
|
|
7
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/metadata/helpers.ts"],"names":[],"mappings":"AAAA,mFAAmF;AAGnF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,OAAmC,CAAC;AAE9E,wCAAwC;AACxC,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,OAC+D,CAAC;AAEhH,iEAAiE;AACjE,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,OAA6E,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Checks whether a field type is an enum reference (not a primitive Edm type). */
|
|
2
|
+
// TODO(#222): duplicate of reso-common's isEnumType — consolidate to one shared helper and
|
|
3
|
+
// prefer the precomputed isEnumeration flag where a field is in hand. See reso-tools #222.
|
|
4
|
+
export const isEnumType = (type) => !type.startsWith('Edm.');
|
|
5
|
+
/** Checks if an Edm type is numeric. */
|
|
6
|
+
export const isNumericEdmType = (type) => ['Edm.Decimal', 'Edm.Int64', 'Edm.Int32', 'Edm.Int16', 'Edm.Double', 'Edm.Single', 'Edm.Byte'].includes(type);
|
|
7
|
+
/** Checks if an Edm type is an integer (no decimals allowed). */
|
|
8
|
+
export const isIntegerEdmType = (type) => ['Edm.Int64', 'Edm.Int32', 'Edm.Int16', 'Edm.Byte'].includes(type);
|
|
9
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/metadata/helpers.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,2FAA2F;AAC3F,2FAA2F;AAC3F,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;AAE9E,wCAAwC;AACxC,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAY,EAAW,EAAE,CACxD,CAAC,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAEhH,iEAAiE;AACjE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { ResoAnnotation, ResoField, ValidationFailure } from './types.js';
|
|
2
|
+
export type { CrossFieldRule, FieldRule } from '../business-rules/index.js';
|
|
3
|
+
export { getBusinessRules, validateBusinessRules } from '../business-rules/index.js';
|
|
4
|
+
export { isEnumType, isIntegerEdmType, isNumericEdmType } from './helpers.js';
|
|
5
|
+
export { validateRecord } from './validate.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/metadata/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/E,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACrF,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/metadata/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACrF,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Annotation entry from RESO metadata (e.g., StandardName, Description, DDWikiUrl). */
|
|
2
|
+
export interface ResoAnnotation {
|
|
3
|
+
readonly term: string;
|
|
4
|
+
readonly value: string;
|
|
5
|
+
}
|
|
6
|
+
/** A field definition from the RESO Data Dictionary metadata. */
|
|
7
|
+
export interface ResoField {
|
|
8
|
+
readonly resourceName: string;
|
|
9
|
+
readonly fieldName: string;
|
|
10
|
+
readonly type: string;
|
|
11
|
+
readonly typeName?: string;
|
|
12
|
+
readonly nullable?: boolean;
|
|
13
|
+
readonly isCollection?: boolean;
|
|
14
|
+
readonly maxLength?: number;
|
|
15
|
+
readonly scale?: number;
|
|
16
|
+
readonly precision?: number;
|
|
17
|
+
readonly annotations: ReadonlyArray<ResoAnnotation>;
|
|
18
|
+
}
|
|
19
|
+
/** A validation failure for a single field. */
|
|
20
|
+
export interface ValidationFailure {
|
|
21
|
+
readonly field: string;
|
|
22
|
+
readonly reason: string;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/metadata/types.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,iEAAiE;AACjE,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;CACrD;AAED,+CAA+C;AAC/C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/metadata/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ResoField, ValidationFailure } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validates a record against the resource's field definitions.
|
|
4
|
+
*
|
|
5
|
+
* Checks for:
|
|
6
|
+
* - Unknown fields (not in metadata)
|
|
7
|
+
* - Null/undefined/empty-string passthrough (no error)
|
|
8
|
+
* - Negative numeric values
|
|
9
|
+
* - MaxLength enforcement for strings
|
|
10
|
+
* - Integer-only enforcement for Int types
|
|
11
|
+
* - Basic type mismatches (string, number, boolean, date, enum, collection)
|
|
12
|
+
*
|
|
13
|
+
* Returns an array of failures (empty if valid).
|
|
14
|
+
*/
|
|
15
|
+
export declare const validateRecord: (body: Readonly<Record<string, unknown>>, fields: ReadonlyArray<ResoField>, skipRequired?: boolean) => ReadonlyArray<ValidationFailure>;
|
|
16
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/metadata/validate.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAK/D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACvC,QAAQ,aAAa,CAAC,SAAS,CAAC,EAChC,sBAAoB,KACnB,aAAa,CAAC,iBAAiB,CAmDjC,CAAC"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { validateBusinessRules } from '../business-rules/index.js';
|
|
2
|
+
import { isEnumType, isNumericEdmType } from './helpers.js';
|
|
3
|
+
/** Fields that legitimately hold negative numeric values (e.g., coordinates). */
|
|
4
|
+
const ALLOW_NEGATIVE_FIELDS = new Set(['Latitude', 'Longitude']);
|
|
5
|
+
/**
|
|
6
|
+
* Validates a record against the resource's field definitions.
|
|
7
|
+
*
|
|
8
|
+
* Checks for:
|
|
9
|
+
* - Unknown fields (not in metadata)
|
|
10
|
+
* - Null/undefined/empty-string passthrough (no error)
|
|
11
|
+
* - Negative numeric values
|
|
12
|
+
* - MaxLength enforcement for strings
|
|
13
|
+
* - Integer-only enforcement for Int types
|
|
14
|
+
* - Basic type mismatches (string, number, boolean, date, enum, collection)
|
|
15
|
+
*
|
|
16
|
+
* Returns an array of failures (empty if valid).
|
|
17
|
+
*/
|
|
18
|
+
export const validateRecord = (body, fields, skipRequired = false) => {
|
|
19
|
+
const fieldMap = new Map(fields.map(f => [f.fieldName, f]));
|
|
20
|
+
const failures = [];
|
|
21
|
+
for (const [key, value] of Object.entries(body)) {
|
|
22
|
+
// Skip OData annotations
|
|
23
|
+
if (key.startsWith('@'))
|
|
24
|
+
continue;
|
|
25
|
+
const field = fieldMap.get(key);
|
|
26
|
+
if (!field) {
|
|
27
|
+
failures.push({
|
|
28
|
+
field: key,
|
|
29
|
+
reason: `'${key}' is not a recognized field. Check field name spelling or consult the metadata.`
|
|
30
|
+
});
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Skip null, undefined, and empty string values
|
|
34
|
+
if (value === null || value === undefined || value === '')
|
|
35
|
+
continue;
|
|
36
|
+
// Check negative numerics (Latitude/Longitude are allowed to be negative)
|
|
37
|
+
if (typeof value === 'number' && value < 0 && isNumericEdmType(field.type) && !ALLOW_NEGATIVE_FIELDS.has(key)) {
|
|
38
|
+
failures.push({ field: key, reason: 'Value must be greater than or equal to 0.' });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
// MaxLength for strings
|
|
42
|
+
if (typeof value === 'string' && field.maxLength && value.length > field.maxLength) {
|
|
43
|
+
failures.push({ field: key, reason: `Exceeds maximum length of ${field.maxLength} characters (currently ${value.length}).` });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Basic type validation
|
|
47
|
+
const typeError = validateFieldType(key, value, field);
|
|
48
|
+
if (typeError) {
|
|
49
|
+
failures.push(typeError);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Field-specific business rules (range constraints, etc.)
|
|
53
|
+
// Skip fields that already have type-level failures to avoid duplicate errors.
|
|
54
|
+
const resourceName = fields[0]?.resourceName;
|
|
55
|
+
if (resourceName) {
|
|
56
|
+
const failedFields = new Set(failures.map(f => f.field));
|
|
57
|
+
const ruleFailures = validateBusinessRules(resourceName, body, skipRequired);
|
|
58
|
+
for (const rf of ruleFailures) {
|
|
59
|
+
if (!failedFields.has(rf.field))
|
|
60
|
+
failures.push(rf);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return failures;
|
|
64
|
+
};
|
|
65
|
+
/** Validates a single field value against its expected Edm type. */
|
|
66
|
+
const validateFieldType = (fieldName, value, field) => {
|
|
67
|
+
if (field.isCollection) {
|
|
68
|
+
if (!Array.isArray(value)) {
|
|
69
|
+
return { field: fieldName, reason: 'Must be a list of values.' };
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if (isEnumType(field.type)) {
|
|
74
|
+
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
75
|
+
return { field: fieldName, reason: 'Please select a valid option from the list.' };
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
switch (field.type) {
|
|
80
|
+
case 'Edm.String':
|
|
81
|
+
if (typeof value !== 'string') {
|
|
82
|
+
return { field: fieldName, reason: 'Must be text.' };
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
case 'Edm.Boolean':
|
|
86
|
+
if (typeof value !== 'boolean') {
|
|
87
|
+
return { field: fieldName, reason: 'Must be true or false.' };
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
case 'Edm.Int64':
|
|
91
|
+
case 'Edm.Int32':
|
|
92
|
+
case 'Edm.Int16':
|
|
93
|
+
case 'Edm.Byte':
|
|
94
|
+
if (typeof value !== 'number') {
|
|
95
|
+
return { field: fieldName, reason: 'Must be a whole number.' };
|
|
96
|
+
}
|
|
97
|
+
if (!Number.isInteger(value)) {
|
|
98
|
+
return { field: fieldName, reason: 'Must be a whole number (no decimals).' };
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
case 'Edm.Decimal':
|
|
102
|
+
case 'Edm.Double':
|
|
103
|
+
case 'Edm.Single':
|
|
104
|
+
if (typeof value !== 'number') {
|
|
105
|
+
return { field: fieldName, reason: 'Must be a number.' };
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
case 'Edm.Date':
|
|
109
|
+
if (typeof value !== 'string') {
|
|
110
|
+
return { field: fieldName, reason: 'Must be a date (YYYY-MM-DD).' };
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
case 'Edm.DateTimeOffset':
|
|
114
|
+
if (typeof value !== 'string') {
|
|
115
|
+
return { field: fieldName, reason: 'Must be a date and time.' };
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/metadata/validate.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reso-standards/reso-validation",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/RESOStandards/reso-tools.git",
|
|
7
|
+
"directory": "reso-validation"
|
|
8
|
+
},
|
|
9
|
+
"description": "RESO validation library — isomorphic, no Node/browser APIs.",
|
|
10
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./metadata": {
|
|
20
|
+
"types": "./dist/metadata/index.d.ts",
|
|
21
|
+
"import": "./dist/metadata/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"prepublishOnly": "npm run build",
|
|
34
|
+
"dev": "tsc --watch",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"lint": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
44
|
+
"typescript": "^5.7.0",
|
|
45
|
+
"vitest": "^4.1.9"
|
|
46
|
+
}
|
|
47
|
+
}
|