@plasius/schema 1.2.5 → 1.2.7

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 CHANGED
@@ -90,6 +90,7 @@ const UserFields = {
90
90
  Common methods (non‑exhaustive): `.required()`, `.optional()`, `.default(v|fn)`, `.description(text)`, and type‑specific helpers like `.email()`, `.uuid()`, `.min()`, `.max()`, `.enum([...])`.
91
91
  Defaults are applied during validation when inputs are missing/`undefined`.
92
92
  Fields are required by default; call `.optional()` (or provide `.default()`) to allow omission.
93
+ Use `.internal()` or `.exposure("internal")` on fields that the server may validate/store but must not include in client-facing payloads.
93
94
 
94
95
  ### 2) Create a **versioned** schema (enforces `type` + `version`)
95
96
 
@@ -144,6 +145,47 @@ if (result.valid && result.errors.length == 0) {
144
145
  - PII strict/warn enforcement runs on nested fields, preventing empty high-PII subfields from slipping through.
145
146
  - Validation deep-clones inputs before applying defaults, so caller-provided objects/arrays aren’t mutated and non-JSON-safe values (e.g., `Date`) are preserved.
146
147
 
148
+ ### 3b) Serialize only client-safe fields
149
+
150
+ Schema serialization is separate from validation. `serialize()` keeps only schema-known fields and drops fields marked `.internal()` unless explicitly requested.
151
+
152
+ ```ts
153
+ const PersistedUserSchema = createSchema(
154
+ {
155
+ id: field.string(),
156
+ partitionKey: field.string().internal(),
157
+ email: field.email(),
158
+ audit: field.object({
159
+ createdBy: field.string().internal(),
160
+ createdAt: field.dateTimeISO(),
161
+ }),
162
+ },
163
+ "persisted-user",
164
+ { version: "1.0.0", piiEnforcement: "strict" }
165
+ );
166
+
167
+ const publicPayload = PersistedUserSchema.serialize({
168
+ type: "persisted-user",
169
+ version: "1.0.0",
170
+ id: "123",
171
+ partitionKey: "tenant-a",
172
+ email: "alice@example.com",
173
+ audit: {
174
+ createdBy: "admin-1",
175
+ createdAt: "2026-03-09T00:00:00.000Z",
176
+ },
177
+ _rid: "cosmos-only",
178
+ });
179
+
180
+ // => {
181
+ // type: "persisted-user",
182
+ // version: "1.0.0",
183
+ // id: "123",
184
+ // email: "alice@example.com",
185
+ // audit: { createdAt: "2026-03-09T00:00:00.000Z" }
186
+ // }
187
+ ```
188
+
147
189
  ### 4) Version enforcement in action
148
190
 
149
191
  If either `type` or `version` doesn’t match the schema, validation fails.
package/dist/index.cjs CHANGED
@@ -60,6 +60,12 @@ __export(index_exports, {
60
60
  module.exports = __toCommonJS(index_exports);
61
61
 
62
62
  // src/field.builder.ts
63
+ function mergeValidatorResult(previousResult, nextValid) {
64
+ if (previousResult !== true) {
65
+ return previousResult;
66
+ }
67
+ return nextValid;
68
+ }
63
69
  var FieldBuilder = class _FieldBuilder {
64
70
  constructor(type, options = {}) {
65
71
  this.type = type;
@@ -86,6 +92,7 @@ var FieldBuilder = class _FieldBuilder {
86
92
  logHandling: "plain",
87
93
  purpose: "an ordinary value"
88
94
  };
95
+ _exposure = "public";
89
96
  enumValues;
90
97
  immutable() {
91
98
  this.isImmutable = true;
@@ -95,6 +102,16 @@ var FieldBuilder = class _FieldBuilder {
95
102
  this.isSystem = true;
96
103
  return this;
97
104
  }
105
+ exposure(exposure) {
106
+ this._exposure = exposure;
107
+ return this;
108
+ }
109
+ internal() {
110
+ return this.exposure("internal");
111
+ }
112
+ public() {
113
+ return this.exposure("public");
114
+ }
98
115
  required() {
99
116
  this.isRequired = true;
100
117
  return this;
@@ -144,19 +161,19 @@ var FieldBuilder = class _FieldBuilder {
144
161
  const prevValidator = this._validator;
145
162
  this._validator = (value) => {
146
163
  const valid = typeof value === "number" && value >= min;
147
- return prevValidator ? prevValidator(value) && valid : valid;
164
+ return prevValidator ? mergeValidatorResult(prevValidator(value), valid) : valid;
148
165
  };
149
166
  } else if (this.type === "string") {
150
167
  const prevValidator = this._validator;
151
168
  this._validator = (value) => {
152
169
  const valid = typeof value === "string" && value.length >= min;
153
- return prevValidator ? prevValidator(value) && valid : valid;
170
+ return prevValidator ? mergeValidatorResult(prevValidator(value), valid) : valid;
154
171
  };
155
172
  } else if (this.type === "array") {
156
173
  const prevValidator = this._validator;
157
174
  this._validator = (value) => {
158
175
  const valid = Array.isArray(value) && value.length >= min;
159
- return prevValidator ? prevValidator(value) && valid : valid;
176
+ return prevValidator ? mergeValidatorResult(prevValidator(value), valid) : valid;
160
177
  };
161
178
  } else {
162
179
  throw new Error(
@@ -170,19 +187,19 @@ var FieldBuilder = class _FieldBuilder {
170
187
  const prevValidator = this._validator;
171
188
  this._validator = (value) => {
172
189
  const valid = typeof value === "number" && value <= max;
173
- return prevValidator ? prevValidator(value) && valid : valid;
190
+ return prevValidator ? mergeValidatorResult(prevValidator(value), valid) : valid;
174
191
  };
175
192
  } else if (this.type === "string") {
176
193
  const prevValidator = this._validator;
177
194
  this._validator = (value) => {
178
195
  const valid = typeof value === "string" && value.length <= max;
179
- return prevValidator ? prevValidator(value) && valid : valid;
196
+ return prevValidator ? mergeValidatorResult(prevValidator(value), valid) : valid;
180
197
  };
181
198
  } else if (this.type === "array") {
182
199
  const prevValidator = this._validator;
183
200
  this._validator = (value) => {
184
201
  const valid = Array.isArray(value) && value.length <= max;
185
- return prevValidator ? prevValidator(value) && valid : valid;
202
+ return prevValidator ? mergeValidatorResult(prevValidator(value), valid) : valid;
186
203
  };
187
204
  } else {
188
205
  throw new Error(
@@ -198,7 +215,7 @@ var FieldBuilder = class _FieldBuilder {
198
215
  const prevValidator = this._validator;
199
216
  this._validator = (value) => {
200
217
  const valid = typeof value === "string" && regex.test(value);
201
- return prevValidator ? prevValidator(value) && valid : valid;
218
+ return prevValidator ? mergeValidatorResult(prevValidator(value), valid) : valid;
202
219
  };
203
220
  return this;
204
221
  }
@@ -229,6 +246,7 @@ var FieldBuilder = class _FieldBuilder {
229
246
  clone._description = this._description;
230
247
  clone._version = this._version;
231
248
  clone._pii = this._pii;
249
+ clone._exposure = this._exposure;
232
250
  clone._validator = this._validator;
233
251
  clone._default = this._default;
234
252
  clone._upgrade = this._upgrade;
@@ -1432,6 +1450,94 @@ function deepClone(value) {
1432
1450
  };
1433
1451
  return cloneAny(value);
1434
1452
  }
1453
+ function shouldIncludeField(def, options) {
1454
+ return options.includeInternal === true || (def?._exposure ?? "public") !== "internal";
1455
+ }
1456
+ function serializeFieldValue(def, value, options) {
1457
+ if (value === void 0) {
1458
+ return void 0;
1459
+ }
1460
+ if (value === null) {
1461
+ return null;
1462
+ }
1463
+ switch (def?.type) {
1464
+ case "string":
1465
+ case "number":
1466
+ case "boolean":
1467
+ return deepClone(value);
1468
+ case "ref": {
1469
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1470
+ return deepClone(value);
1471
+ }
1472
+ const shape = getShape(def);
1473
+ if (!shape || Object.keys(shape).length === 0) {
1474
+ return deepClone(value);
1475
+ }
1476
+ const serializedRef = {};
1477
+ if (typeof value.type === "string") {
1478
+ serializedRef.type = value.type;
1479
+ }
1480
+ if (typeof value.id === "string") {
1481
+ serializedRef.id = value.id;
1482
+ }
1483
+ for (const [childKey, childDef] of Object.entries(shape)) {
1484
+ if (!shouldIncludeField(childDef, options)) {
1485
+ continue;
1486
+ }
1487
+ if (!Object.prototype.hasOwnProperty.call(value, childKey)) {
1488
+ continue;
1489
+ }
1490
+ const serializedChild = serializeFieldValue(
1491
+ childDef,
1492
+ value[childKey],
1493
+ options
1494
+ );
1495
+ if (serializedChild !== void 0) {
1496
+ serializedRef[childKey] = serializedChild;
1497
+ }
1498
+ }
1499
+ return serializedRef;
1500
+ }
1501
+ case "object": {
1502
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1503
+ return deepClone(value);
1504
+ }
1505
+ const shape = getShape(def);
1506
+ if (!shape || Object.keys(shape).length === 0) {
1507
+ return deepClone(value);
1508
+ }
1509
+ const serializedObject = {};
1510
+ for (const [childKey, childDef] of Object.entries(shape)) {
1511
+ if (!shouldIncludeField(childDef, options)) {
1512
+ continue;
1513
+ }
1514
+ if (!Object.prototype.hasOwnProperty.call(value, childKey)) {
1515
+ continue;
1516
+ }
1517
+ const serializedChild = serializeFieldValue(
1518
+ childDef,
1519
+ value[childKey],
1520
+ options
1521
+ );
1522
+ if (serializedChild !== void 0) {
1523
+ serializedObject[childKey] = serializedChild;
1524
+ }
1525
+ }
1526
+ return serializedObject;
1527
+ }
1528
+ case "array": {
1529
+ if (!Array.isArray(value)) {
1530
+ return deepClone(value);
1531
+ }
1532
+ if (!def.itemType) {
1533
+ return deepClone(value);
1534
+ }
1535
+ return value.map((item) => serializeFieldValue(def.itemType, item, options));
1536
+ }
1537
+ default:
1538
+ return deepClone(value);
1539
+ }
1540
+ }
1435
1541
  function cmpSemver(a, b) {
1436
1542
  const pa = a.split(".").map((n) => parseInt(n, 10));
1437
1543
  const pb = b.split(".").map((n) => parseInt(n, 10));
@@ -1508,6 +1614,38 @@ function getValidator(def) {
1508
1614
  function getShape(def) {
1509
1615
  return def?._shape ?? def?.shape ?? void 0;
1510
1616
  }
1617
+ function normalizeValidationIssue(path, issue, defaultCode) {
1618
+ if (typeof issue === "string") {
1619
+ return {
1620
+ path,
1621
+ code: defaultCode,
1622
+ message: issue
1623
+ };
1624
+ }
1625
+ return {
1626
+ path: issue.path ?? path,
1627
+ code: issue.code || defaultCode,
1628
+ message: issue.message
1629
+ };
1630
+ }
1631
+ function normalizeValidatorIssues(path, result, defaultCode) {
1632
+ if (result === true) {
1633
+ return [];
1634
+ }
1635
+ if (result === false) {
1636
+ return [
1637
+ {
1638
+ path,
1639
+ code: defaultCode,
1640
+ message: `Invalid value for field: ${path}`
1641
+ }
1642
+ ];
1643
+ }
1644
+ if (Array.isArray(result)) {
1645
+ return result.map((issue) => normalizeValidationIssue(path, issue, defaultCode));
1646
+ }
1647
+ return [normalizeValidationIssue(path, result, defaultCode)];
1648
+ }
1511
1649
  function checkMissingRequired(parentKey, key, value, def, errors) {
1512
1650
  if (value === void 0 || value === null) {
1513
1651
  const path = parentKey ? `${parentKey}.${key}` : key;
@@ -1526,13 +1664,16 @@ function checkImmutable(parentKey, key, value, def, existing, errors) {
1526
1664
  }
1527
1665
  return { immutableViolation: false };
1528
1666
  }
1529
- function runCustomValidator(parentKey, key, value, def, errors) {
1667
+ function runCustomValidator(parentKey, key, value, def, errors, issues) {
1530
1668
  const validator = getValidator(def);
1531
1669
  if (validator && value !== void 0 && value !== null) {
1532
- const valid = validator(value);
1533
- if (!valid) {
1534
- const path = parentKey ? `${parentKey}.${key}` : key;
1535
- errors.push(`Invalid value for field: ${path}`);
1670
+ const path = parentKey ? `${parentKey}.${key}` : key;
1671
+ const normalizedIssues = normalizeValidatorIssues(path, validator(value), "invalid_value");
1672
+ if (normalizedIssues.length > 0) {
1673
+ errors.push(...normalizedIssues.map((issue) => issue.message));
1674
+ if (issues) {
1675
+ issues.push(...normalizedIssues);
1676
+ }
1536
1677
  return { invalid: true };
1537
1678
  }
1538
1679
  }
@@ -1572,7 +1713,7 @@ function validateBooleanField(parentKey, key, value, _def, errors) {
1572
1713
  errors.push(`Field ${path} must be boolean`);
1573
1714
  }
1574
1715
  }
1575
- function validateObjectChildren(parentKey, obj, shape, errors, existing, piiEnforcement, logger) {
1716
+ function validateObjectChildren(parentKey, obj, shape, errors, issues, existing, piiEnforcement, logger) {
1576
1717
  for (const [childKey, childDef] of Object.entries(shape)) {
1577
1718
  let childValue = obj[childKey];
1578
1719
  const existingChild = existing && typeof existing === "object" ? existing[childKey] : void 0;
@@ -1613,7 +1754,8 @@ function validateObjectChildren(parentKey, obj, shape, errors, existing, piiEnfo
1613
1754
  childKey,
1614
1755
  childValue,
1615
1756
  childDef,
1616
- errors
1757
+ errors,
1758
+ issues
1617
1759
  );
1618
1760
  if (invalid) continue;
1619
1761
  validateByType(
@@ -1622,13 +1764,14 @@ function validateObjectChildren(parentKey, obj, shape, errors, existing, piiEnfo
1622
1764
  childValue,
1623
1765
  childDef,
1624
1766
  errors,
1767
+ issues,
1625
1768
  existingChild,
1626
1769
  piiEnforcement,
1627
1770
  logger
1628
1771
  );
1629
1772
  }
1630
1773
  }
1631
- function validateObjectField(parentKey, key, value, def, errors, existing, piiEnforcement, logger) {
1774
+ function validateObjectField(parentKey, key, value, def, errors, issues, existing, piiEnforcement, logger) {
1632
1775
  const path = parentKey ? `${parentKey}.${key}` : key;
1633
1776
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
1634
1777
  errors.push(`Field ${path} must be object`);
@@ -1636,7 +1779,7 @@ function validateObjectField(parentKey, key, value, def, errors, existing, piiEn
1636
1779
  }
1637
1780
  const objShape = getShape(def);
1638
1781
  if (objShape)
1639
- validateObjectChildren(path, value, objShape, errors, existing, piiEnforcement, logger);
1782
+ validateObjectChildren(path, value, objShape, errors, issues, existing, piiEnforcement, logger);
1640
1783
  }
1641
1784
  function validateArrayOfStrings(parentKey, key, arr, itemDef, errors) {
1642
1785
  const path = parentKey ? `${parentKey}.${key}` : key;
@@ -1695,7 +1838,7 @@ function validateArrayOfBooleans(parentKey, key, arr, _itemDef, errors) {
1695
1838
  });
1696
1839
  }
1697
1840
  }
1698
- function validateArrayOfObjects(parentKey, key, arr, itemDef, errors, existing, piiEnforcement, logger) {
1841
+ function validateArrayOfObjects(parentKey, key, arr, itemDef, errors, issues, existing, piiEnforcement, logger) {
1699
1842
  const path = parentKey ? `${parentKey}.${key}` : key;
1700
1843
  if (!Array.isArray(arr) || !arr.every((v) => typeof v === "object" && v !== null && !Array.isArray(v))) {
1701
1844
  errors.push(`Field ${path} must be object[]`);
@@ -1749,7 +1892,8 @@ function validateArrayOfObjects(parentKey, key, arr, itemDef, errors, existing,
1749
1892
  childKey,
1750
1893
  childValue,
1751
1894
  childDef,
1752
- errors
1895
+ errors,
1896
+ issues
1753
1897
  );
1754
1898
  if (invalid) continue;
1755
1899
  validateByType(
@@ -1758,6 +1902,7 @@ function validateArrayOfObjects(parentKey, key, arr, itemDef, errors, existing,
1758
1902
  childValue,
1759
1903
  childDef,
1760
1904
  errors,
1905
+ issues,
1761
1906
  existingChild,
1762
1907
  piiEnforcement,
1763
1908
  logger
@@ -1765,7 +1910,7 @@ function validateArrayOfObjects(parentKey, key, arr, itemDef, errors, existing,
1765
1910
  }
1766
1911
  });
1767
1912
  }
1768
- function validateArrayField(parentKey, key, value, def, errors, existing, piiEnforcement, logger) {
1913
+ function validateArrayField(parentKey, key, value, def, errors, issues, existing, piiEnforcement, logger) {
1769
1914
  const path = parentKey ? `${parentKey}.${key}` : key;
1770
1915
  if (!Array.isArray(value)) {
1771
1916
  errors.push(`Field ${key} must be an array`);
@@ -1785,6 +1930,7 @@ function validateArrayField(parentKey, key, value, def, errors, existing, piiEnf
1785
1930
  value,
1786
1931
  def.itemType,
1787
1932
  errors,
1933
+ issues,
1788
1934
  existing,
1789
1935
  piiEnforcement,
1790
1936
  logger
@@ -1841,9 +1987,14 @@ function validateArrayField(parentKey, key, value, def, errors, existing, piiEnf
1841
1987
  if (shortCircuit) continue;
1842
1988
  const childValidator = getValidator(childDef);
1843
1989
  if (childValidator && childValue !== void 0 && childValue !== null) {
1844
- const valid = childValidator(childValue);
1845
- if (!valid) {
1846
- errors.push(`Invalid value for field: ${childPath}`);
1990
+ const normalizedIssues = normalizeValidatorIssues(
1991
+ childPath,
1992
+ childValidator(childValue),
1993
+ "invalid_value"
1994
+ );
1995
+ if (normalizedIssues.length > 0) {
1996
+ errors.push(...normalizedIssues.map((issue) => issue.message));
1997
+ issues.push(...normalizedIssues);
1847
1998
  continue;
1848
1999
  }
1849
2000
  }
@@ -1853,6 +2004,7 @@ function validateArrayField(parentKey, key, value, def, errors, existing, piiEnf
1853
2004
  childValue,
1854
2005
  childDef,
1855
2006
  errors,
2007
+ issues,
1856
2008
  existingChild,
1857
2009
  piiEnforcement,
1858
2010
  logger
@@ -1878,7 +2030,7 @@ function validateRefField(parentKey, key, value, def, errors, _existing, _piiEnf
1878
2030
  );
1879
2031
  }
1880
2032
  }
1881
- function validateByType(parentKey, key, value, def, errors, existing, piiEnforcement, logger) {
2033
+ function validateByType(parentKey, key, value, def, errors, issues, existing, piiEnforcement, logger) {
1882
2034
  const path = parentKey ? `${parentKey}.${key}` : key;
1883
2035
  switch (def.type) {
1884
2036
  case "string":
@@ -1894,6 +2046,7 @@ function validateByType(parentKey, key, value, def, errors, existing, piiEnforce
1894
2046
  value,
1895
2047
  def,
1896
2048
  errors,
2049
+ issues,
1897
2050
  existing,
1898
2051
  piiEnforcement,
1899
2052
  logger
@@ -1905,6 +2058,7 @@ function validateByType(parentKey, key, value, def, errors, existing, piiEnforce
1905
2058
  value,
1906
2059
  def,
1907
2060
  errors,
2061
+ issues,
1908
2062
  existing,
1909
2063
  piiEnforcement,
1910
2064
  logger
@@ -1940,6 +2094,7 @@ function createSchema(_shape, entityType, options = {
1940
2094
  // 🔗 Validate input against the schema
1941
2095
  validate(input, existing) {
1942
2096
  const errors = [];
2097
+ const issues = [];
1943
2098
  const result = {};
1944
2099
  const piiMode = options.piiEnforcement ?? "none";
1945
2100
  if (typeof input !== "object" || input === null) {
@@ -2003,16 +2158,19 @@ function createSchema(_shape, entityType, options = {
2003
2158
  if (shortCircuit) continue;
2004
2159
  const validateField = (val) => {
2005
2160
  const localErrors = [];
2161
+ const localIssues = [];
2006
2162
  const { invalid } = runCustomValidator(
2007
2163
  "",
2008
2164
  key,
2009
2165
  val,
2010
2166
  def,
2011
- localErrors
2167
+ localErrors,
2168
+ localIssues
2012
2169
  );
2013
2170
  if (!invalid) {
2014
- validateByType("", key, val, def, localErrors, existingField, piiMode, console);
2171
+ validateByType("", key, val, def, localErrors, localIssues, existingField, piiMode, console);
2015
2172
  }
2173
+ issues.push(...localIssues);
2016
2174
  return localErrors;
2017
2175
  };
2018
2176
  let fieldValue = value;
@@ -2053,7 +2211,8 @@ function createSchema(_shape, entityType, options = {
2053
2211
  return {
2054
2212
  valid: errors.length === 0,
2055
2213
  value: result,
2056
- errors
2214
+ errors,
2215
+ issues
2057
2216
  };
2058
2217
  },
2059
2218
  // specific validator for a schema to allow conditional validation
@@ -2203,6 +2362,25 @@ function createSchema(_shape, entityType, options = {
2203
2362
  }
2204
2363
  return store;
2205
2364
  },
2365
+ serialize(input, options2 = {}) {
2366
+ if (typeof input !== "object" || input === null) {
2367
+ return {};
2368
+ }
2369
+ const serialized = {};
2370
+ for (const [key, def] of Object.entries(schema._shape)) {
2371
+ if (!shouldIncludeField(def, options2)) {
2372
+ continue;
2373
+ }
2374
+ if (!Object.prototype.hasOwnProperty.call(input, key)) {
2375
+ continue;
2376
+ }
2377
+ const value = serializeFieldValue(def, input[key], options2);
2378
+ if (value !== void 0) {
2379
+ serialized[key] = value;
2380
+ }
2381
+ }
2382
+ return serialized;
2383
+ },
2206
2384
  /**
2207
2385
  * Transforms an input object for persistence by applying PII protection
2208
2386
  * according to field annotations (e.g., encryption and hashing).
@@ -2272,7 +2450,8 @@ function createSchema(_shape, entityType, options = {
2272
2450
  deprecatedVersion: def.deprecatedVersion === void 0 ? null : def.deprecatedVersion,
2273
2451
  enum: getEnumValues(def) ?? null,
2274
2452
  refType: def.refType ?? null,
2275
- pii: !def._pii || def._pii.classification === "none" ? null : def._pii
2453
+ pii: !def._pii || def._pii.classification === "none" ? null : def._pii,
2454
+ exposure: def._exposure ?? "public"
2276
2455
  };
2277
2456
  }
2278
2457
  return {
@@ -2302,7 +2481,8 @@ function renderSchemaDescription(schema) {
2302
2481
  optional: def.optional,
2303
2482
  description: def.description,
2304
2483
  deprecated: def.deprecated,
2305
- pii: def.pii ? def.pii.classification : void 0
2484
+ pii: def.pii ? def.pii.classification : void 0,
2485
+ exposure: def.exposure
2306
2486
  }))
2307
2487
  };
2308
2488
  }