@metaobjectsdev/runtime-ts 1.0.13 → 1.1.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/decimal-wire.d.ts +54 -0
  2. package/dist/decimal-wire.d.ts.map +1 -0
  3. package/dist/decimal-wire.js +158 -0
  4. package/dist/decimal-wire.js.map +1 -0
  5. package/dist/drizzle-fastify/filter-allowlist.d.ts +4 -2
  6. package/dist/drizzle-fastify/filter-allowlist.d.ts.map +1 -1
  7. package/dist/drizzle-fastify/filter-allowlist.js +2 -1
  8. package/dist/drizzle-fastify/filter-allowlist.js.map +1 -1
  9. package/dist/drizzle-fastify/filter-parser.d.ts.map +1 -1
  10. package/dist/drizzle-fastify/filter-parser.js +23 -9
  11. package/dist/drizzle-fastify/filter-parser.js.map +1 -1
  12. package/dist/drizzle-fastify/filter-value-format.d.ts.map +1 -1
  13. package/dist/drizzle-fastify/filter-value-format.js +4 -1
  14. package/dist/drizzle-fastify/filter-value-format.js.map +1 -1
  15. package/dist/drizzle-fastify/index.d.ts +8 -0
  16. package/dist/drizzle-fastify/index.d.ts.map +1 -1
  17. package/dist/drizzle-fastify/index.js +4 -2
  18. package/dist/drizzle-fastify/index.js.map +1 -1
  19. package/dist/drizzle-fastify/mount-m2m.d.ts +2 -0
  20. package/dist/drizzle-fastify/mount-m2m.d.ts.map +1 -1
  21. package/dist/drizzle-fastify/mount-m2m.js +2 -1
  22. package/dist/drizzle-fastify/mount-m2m.js.map +1 -1
  23. package/dist/drizzle-fastify/mount-read-only.d.ts +25 -1
  24. package/dist/drizzle-fastify/mount-read-only.d.ts.map +1 -1
  25. package/dist/drizzle-fastify/mount-read-only.js +43 -28
  26. package/dist/drizzle-fastify/mount-read-only.js.map +1 -1
  27. package/dist/extract-object.js +5 -7
  28. package/dist/extract-object.js.map +1 -1
  29. package/dist/hono/index.d.ts +8 -0
  30. package/dist/hono/index.d.ts.map +1 -1
  31. package/dist/hono/index.js +4 -2
  32. package/dist/hono/index.js.map +1 -1
  33. package/dist/hono/mount-read-only.d.ts +25 -1
  34. package/dist/hono/mount-read-only.d.ts.map +1 -1
  35. package/dist/hono/mount-read-only.js +51 -36
  36. package/dist/hono/mount-read-only.js.map +1 -1
  37. package/dist/index.d.ts +2 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -0
  40. package/dist/index.js.map +1 -1
  41. package/dist/n2m-resolver.d.ts.map +1 -1
  42. package/dist/n2m-resolver.js +4 -3
  43. package/dist/n2m-resolver.js.map +1 -1
  44. package/dist/net-format.d.ts +8 -0
  45. package/dist/net-format.d.ts.map +1 -0
  46. package/dist/net-format.js +34 -0
  47. package/dist/net-format.js.map +1 -0
  48. package/dist/object-manager.d.ts +20 -7
  49. package/dist/object-manager.d.ts.map +1 -1
  50. package/dist/object-manager.js +68 -13
  51. package/dist/object-manager.js.map +1 -1
  52. package/dist/query-builder.d.ts.map +1 -1
  53. package/dist/query-builder.js +11 -2
  54. package/dist/query-builder.js.map +1 -1
  55. package/dist/relation-resolver.d.ts.map +1 -1
  56. package/dist/relation-resolver.js +11 -11
  57. package/dist/relation-resolver.js.map +1 -1
  58. package/dist/type-coercer.d.ts +1 -1
  59. package/dist/type-coercer.d.ts.map +1 -1
  60. package/dist/type-coercer.js +114 -5
  61. package/dist/type-coercer.js.map +1 -1
  62. package/dist/validator-runner.d.ts.map +1 -1
  63. package/dist/validator-runner.js +192 -35
  64. package/dist/validator-runner.js.map +1 -1
  65. package/package.json +3 -3
  66. package/src/decimal-wire.ts +154 -0
  67. package/src/drizzle-fastify/filter-allowlist.ts +5 -1
  68. package/src/drizzle-fastify/filter-parser.ts +22 -9
  69. package/src/drizzle-fastify/filter-value-format.ts +4 -0
  70. package/src/drizzle-fastify/index.ts +11 -2
  71. package/src/drizzle-fastify/mount-m2m.ts +4 -1
  72. package/src/drizzle-fastify/mount-read-only.ts +68 -31
  73. package/src/extract-object.ts +5 -7
  74. package/src/hono/index.ts +11 -2
  75. package/src/hono/mount-read-only.ts +76 -37
  76. package/src/index.ts +2 -1
  77. package/src/n2m-resolver.ts +4 -2
  78. package/src/net-format.ts +37 -0
  79. package/src/object-manager.ts +80 -18
  80. package/src/query-builder.ts +14 -1
  81. package/src/relation-resolver.ts +15 -14
  82. package/src/type-coercer.ts +116 -5
  83. package/src/validator-runner.ts +195 -32
@@ -1,19 +1,25 @@
1
1
  // Pure function: NEVER throws. ObjectManager wraps a non-ok result in a ValidationError on writes;
2
2
  // om.validate() returns the result directly.
3
3
 
4
- import { isMetaRoot, type MetaData } from "@metaobjectsdev/metadata";
4
+ import { isMetaObject, isMetaRoot, refTarget, type MetaData } from "@metaobjectsdev/metadata";
5
5
  import {
6
6
  TYPE_FIELD, TYPE_VALIDATOR,
7
7
  VALIDATOR_SUBTYPE_REQUIRED, VALIDATOR_SUBTYPE_LENGTH, VALIDATOR_SUBTYPE_REGEX,
8
8
  FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG,
9
9
  FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, FIELD_SUBTYPE_CURRENCY,
10
- FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_UUID, FIELD_SUBTYPE_OBJECT,
10
+ FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_UUID, FIELD_SUBTYPE_OBJECT, FIELD_SUBTYPE_MAP, FIELD_SUBTYPE_DECIMAL,
11
+ FIELD_ATTR_PRECISION, FIELD_ATTR_SCALE,
11
12
  FIELD_ATTR_REQUIRED, FIELD_ATTR_MAX_LENGTH, FIELD_ATTR_DEFAULT,
12
13
  FIELD_ATTR_DB_COLUMN_TYPE, DB_COLUMN_TYPE_JSONB, FIELD_ATTR_OBJECT_REF,
13
- PACKAGE_SEPARATOR, OBJECT_SUBTYPE_VALUE,
14
+ OBJECT_SUBTYPE_VALUE,
14
15
  VALIDATOR_ATTR_MIN, VALIDATOR_ATTR_MAX, VALIDATOR_ATTR_PATTERN,
16
+ VALIDATOR_SUBTYPE_NUMERIC, VALIDATOR_SUBTYPE_ARRAY,
17
+ FIELD_SUBTYPE_URI, FIELD_SUBTYPE_INET, FIELD_ATTR_LENIENT,
18
+ IDENTITY_ATTR_FIELDS, IDENTITY_ATTR_GENERATION, GENERATION_INCREMENT, GENERATION_UUID,
15
19
  } from "@metaobjectsdev/metadata";
16
20
  import type { ValidationFailure } from "./errors.js";
21
+ import { decimalFits, parseDecimalInput } from "./decimal-wire.js";
22
+ import { isAbsoluteUri, isInetLiteral } from "./net-format.js";
17
23
 
18
24
  export type ValidationResult =
19
25
  | { ok: true }
@@ -56,6 +62,7 @@ export function runValidators(
56
62
  opts: RunValidatorsOpts = {},
57
63
  ): ValidationResult {
58
64
  const errors: ValidationFailure[] = [];
65
+ const assignedPk = assignedPkFieldNames(entity);
59
66
 
60
67
  // Effective children so a TPH subtype validates inherited base fields too.
61
68
  for (const field of entity.children()) {
@@ -70,7 +77,11 @@ export function runValidators(
70
77
  const required = isRequired(field);
71
78
  // ADR-0039: effective attr — @default may be inherited via extends.
72
79
  const hasDefault = field.attr(FIELD_ATTR_DEFAULT) !== undefined;
73
- if (required && (value === undefined || value === null)) {
80
+ // An ASSIGNED primary key must be supplied whatever @required says: nothing else can
81
+ // produce the value. That is presence only — the non-empty-string floor below stays
82
+ // tied to a DECLARED required, as in the generated InsertSchema.
83
+ const mustBePresent = required || assignedPk.has(field.name);
84
+ if (mustBePresent && (value === undefined || value === null)) {
74
85
  if (opts.partial && !present) continue;
75
86
  // A @default exempts a required field only when it is ABSENT (the DB fills
76
87
  // it on insert / an omitted patch key is untouched). It does NOT rescue an
@@ -117,6 +128,7 @@ export function runValidators(
117
128
  continue;
118
129
  }
119
130
  const elements: unknown[] = field.resolvedIsArray() ? (value as unknown[]) : [value];
131
+ if (field.resolvedIsArray()) errors.push(...arraySizeErrors(field, elements.length));
120
132
  elements.forEach((el, i) => {
121
133
  if (typeof el !== "object" || el === null || Array.isArray(el)) {
122
134
  errors.push({
@@ -140,6 +152,38 @@ export function runValidators(
140
152
  continue;
141
153
  }
142
154
 
155
+ // A map of value objects (`field.map @objectRef`): every map value is a VO validated in full,
156
+ // as an array element is. The keys are arbitrary text and are never compared with the VO's
157
+ // member names. A map whose value is a scalar (@valueType) is unchanged.
158
+ if (field.subType === FIELD_SUBTYPE_MAP) {
159
+ const vo = resolveVoRef(field);
160
+ if (vo !== undefined) {
161
+ if (typeof value !== "object" || Array.isArray(value)) {
162
+ errors.push({
163
+ field: field.name, rule: "type",
164
+ message: `'${field.name}' must be a map of ${vo.name}`,
165
+ expected: "object", received: Array.isArray(value) ? "array" : typeof value,
166
+ });
167
+ continue;
168
+ }
169
+ for (const [key, el] of Object.entries(value as Record<string, unknown>)) {
170
+ if (typeof el !== "object" || el === null || Array.isArray(el)) {
171
+ errors.push({
172
+ field: `${field.name}.${key}`, rule: "type",
173
+ message: `'${field.name}.${key}' must be a ${vo.name} object`,
174
+ expected: vo.name, received: el === null ? "null" : Array.isArray(el) ? "array" : typeof el,
175
+ });
176
+ continue;
177
+ }
178
+ const sub = runValidators(vo, el as Record<string, unknown>);
179
+ if (!sub.ok) {
180
+ for (const e of sub.errors) errors.push({ ...e, field: `${field.name}.${key}.${e.field}` });
181
+ }
182
+ }
183
+ continue;
184
+ }
185
+ }
186
+
143
187
  // A scalar array (`field.string isArray`, …) — stored as a native array or a JSON array.
144
188
  // Each element is checked against the element rules; an element error names its index,
145
189
  // as the value-object branch above does. ADR-0039: resolving `isArray`.
@@ -152,6 +196,7 @@ export function runValidators(
152
196
  });
153
197
  continue;
154
198
  }
199
+ errors.push(...arraySizeErrors(field, value.length));
155
200
  value.forEach((el, i) => {
156
201
  if (el === null || el === undefined) return;
157
202
  errors.push(...scalarErrors(field, el, false, `${field.name}[${i}]`));
@@ -166,8 +211,7 @@ export function runValidators(
166
211
  }
167
212
 
168
213
  /** Resolve a `field.object`'s `@objectRef` to its value-object MetaData by walking
169
- * to the tree root. The ref may be a bare name or a `pkg::Name` FQN. Mirrors the
170
- * extract-object resolver. Returns undefined when unresolvable OR when the target
214
+ * to the tree root. The ref may be a bare name or a `pkg::Name` FQN. Returns undefined when unresolvable OR when the target
171
215
  * is not an `object.value` (→ no VO recursion). Cross-port parity: C#/Java/Kotlin
172
216
  * gate the recursion on the ref being a value object, so a `field.object @objectRef`
173
217
  * pointing at a non-value object validates identically (skipped) on every port.
@@ -180,11 +224,9 @@ function resolveVoRef(field: MetaData): MetaData | undefined {
180
224
  // class check fails for a real root and every VO reference silently stops
181
225
  // resolving, skipping nested value-object validation with no error.
182
226
  if (!isMetaRoot(root)) return undefined;
183
- let target = root.findObject(ref);
184
- if (target === undefined) {
185
- const sep = ref.lastIndexOf(PACKAGE_SEPARATOR);
186
- if (sep >= 0) target = root.findObject(ref.slice(sep + PACKAGE_SEPARATOR.length));
187
- }
227
+ // ADR-0059 R7 — the object the loader bound `@objectRef` to, resolved in the package
228
+ // of the node that declared it — never a same-named object in some other package.
229
+ const target = refTarget(field, FIELD_ATTR_OBJECT_REF);
188
230
  return target?.subType === OBJECT_SUBTYPE_VALUE ? target : undefined;
189
231
  }
190
232
 
@@ -197,32 +239,87 @@ function isRequired(field: MetaData): boolean {
197
239
  return false;
198
240
  }
199
241
 
200
- function resolveMaxLength(field: MetaData): number | undefined {
201
- // ADR-0039: effective — @maxLength and a length-validator may be inherited via extends.
202
- const attr = field.attr(FIELD_ATTR_MAX_LENGTH);
203
- if (typeof attr === "number") return attr;
242
+ /** Primary-identity field names the CALLER must supply: the identity carries no
243
+ * store-side `@generation` (increment / uuid). A `@default` on the field still lets the
244
+ * caller omit it — the `hasDefault` exemption at the call site covers that. */
245
+ function assignedPkFieldNames(entity: MetaData): Set<string> {
246
+ // isMetaObject, not `instanceof` — see resolveVoRef.
247
+ const primary = isMetaObject(entity) ? entity.primaryIdentity() : undefined;
248
+ if (primary === undefined) return new Set();
249
+ // ADR-0039: effective — an identity may be inherited via extends.
250
+ const generation = primary.attr(IDENTITY_ATTR_GENERATION);
251
+ if (generation === GENERATION_INCREMENT || generation === GENERATION_UUID) return new Set();
252
+ const fields = primary.attr(IDENTITY_ATTR_FIELDS);
253
+ if (Array.isArray(fields)) return new Set(fields.map(String));
254
+ return typeof fields === "string" ? new Set([fields]) : new Set();
255
+ }
256
+
257
+ interface Bounds { min?: number; max?: number }
258
+
259
+ /** The `@min` / `@max` of the field's validators of one subtype (last authored wins). */
260
+ function validatorBounds(field: MetaData, validatorSubType: string): Bounds {
261
+ const bounds: Bounds = {};
262
+ // ADR-0039: effective — a validator and its bounds may be inherited via extends.
204
263
  for (const child of field.children()) {
205
- if (child.type !== TYPE_VALIDATOR) continue;
206
- if (child.subType !== VALIDATOR_SUBTYPE_LENGTH) continue;
264
+ if (child.type !== TYPE_VALIDATOR || child.subType !== validatorSubType) continue;
265
+ const min = child.attr(VALIDATOR_ATTR_MIN);
207
266
  const max = child.attr(VALIDATOR_ATTR_MAX);
208
- if (typeof max === "number") return max;
267
+ if (typeof min === "number") bounds.min = min;
268
+ if (typeof max === "number") bounds.max = max;
209
269
  }
210
- return undefined;
270
+ return bounds;
211
271
  }
212
272
 
213
- function resolveMinLength(field: MetaData): number | undefined {
214
- // ADR-0039: effective — a length-validator may be inherited via extends.
273
+ /** String length bounds. Max is strictest-wins across `@maxLength` and every
274
+ * `validator.length @max` (FR-036 A3). Min is the authored `validator.length @min`,
275
+ * undefined when none was authored. */
276
+ function lengthBounds(field: MetaData): Bounds {
277
+ const bounds: Bounds = {};
278
+ // ADR-0039: effective — @maxLength and a length-validator may be inherited via extends.
279
+ const attr = field.attr(FIELD_ATTR_MAX_LENGTH);
280
+ if (typeof attr === "number") bounds.max = attr;
215
281
  for (const child of field.children()) {
216
- if (child.type !== TYPE_VALIDATOR) continue;
217
- if (child.subType !== VALIDATOR_SUBTYPE_LENGTH) continue;
282
+ if (child.type !== TYPE_VALIDATOR || child.subType !== VALIDATOR_SUBTYPE_LENGTH) continue;
218
283
  const min = child.attr(VALIDATOR_ATTR_MIN);
219
- if (typeof min === "number") return min;
284
+ const max = child.attr(VALIDATOR_ATTR_MAX);
285
+ if (typeof min === "number") bounds.min = min;
286
+ if (typeof max === "number") bounds.max = bounds.max === undefined ? max : Math.min(bounds.max, max);
287
+ }
288
+ return bounds;
289
+ }
290
+
291
+ /** `validator.array @min/@max` — element-count bounds on an array field of any element type. */
292
+ function arraySizeErrors(field: MetaData, size: number): ValidationFailure[] {
293
+ const { min, max } = validatorBounds(field, VALIDATOR_SUBTYPE_ARRAY);
294
+ const errors: ValidationFailure[] = [];
295
+ if (min !== undefined && size < min) {
296
+ errors.push({
297
+ field: field.name, rule: "array",
298
+ message: `'${field.name}' must have at least ${min} items (got ${size})`,
299
+ expected: { min }, received: size,
300
+ });
220
301
  }
302
+ if (max !== undefined && size > max) {
303
+ errors.push({
304
+ field: field.name, rule: "array",
305
+ message: `'${field.name}' must have at most ${max} items (got ${size})`,
306
+ expected: { max }, received: size,
307
+ });
308
+ }
309
+ return errors;
310
+ }
311
+
312
+ /** The comparable numeric value of a type-checked numeric field value. An int64 arrives
313
+ * as a number, a bigint or a base-10 integer string; the string compares as an integer. */
314
+ function comparable(value: unknown): number | bigint | undefined {
315
+ if (typeof value === "number" || typeof value === "bigint") return value;
316
+ if (typeof value === "string" && INT64_STRING_RE.test(value)) return BigInt(value);
221
317
  return undefined;
222
318
  }
223
319
 
224
320
  function checkType(subType: string, value: unknown): string | null {
225
- if (subType === FIELD_SUBTYPE_STRING || subType === FIELD_SUBTYPE_UUID) {
321
+ if (subType === FIELD_SUBTYPE_STRING || subType === FIELD_SUBTYPE_UUID
322
+ || subType === FIELD_SUBTYPE_URI || subType === FIELD_SUBTYPE_INET) {
226
323
  if (typeof value !== "string") return `expected string`;
227
324
  } else if (NUMERIC_FIELD_SUBTYPES.has(subType)) {
228
325
  if (typeof value !== "number") return `expected number`;
@@ -237,6 +334,33 @@ function checkType(subType: string, value: unknown): string | null {
237
334
  return null;
238
335
  }
239
336
 
337
+ /**
338
+ * ADR-0019 Amendment 1 (W2, W3) on a write: a decimal is a string in the JSON-number grammar or a
339
+ * JSON number a double holds exactly, and must fit the declared `@precision` / `@scale`. Refused
340
+ * here, as a validation failure, so the write answers 400 rather than failing later in the coercer.
341
+ */
342
+ function decimalErrors(field: MetaData, value: unknown, label: string): ValidationFailure[] {
343
+ const canonical = parseDecimalInput(value);
344
+ if (canonical === undefined) {
345
+ return [{
346
+ field: label, rule: "type",
347
+ message: `'${label}' must be a decimal number (a string in the JSON-number grammar, or a number a double holds exactly: send a string for more than 15 significant digits)`,
348
+ expected: FIELD_SUBTYPE_DECIMAL, received: typeof value,
349
+ }];
350
+ }
351
+ // ADR-0039: effective attrs — @precision / @scale may be inherited via extends.
352
+ const precision = field.attr(FIELD_ATTR_PRECISION);
353
+ const scale = field.attr(FIELD_ATTR_SCALE);
354
+ if (!decimalFits(canonical, typeof precision === "number" ? precision : undefined, typeof scale === "number" ? scale : undefined)) {
355
+ return [{
356
+ field: label, rule: "range",
357
+ message: `'${label}' (${canonical}) does not fit @precision ${String(precision)} / @scale ${String(scale ?? 0)}`,
358
+ expected: { precision, scale }, received: canonical,
359
+ }];
360
+ }
361
+ return [];
362
+ }
363
+
240
364
  /**
241
365
  * The type, length and regex errors for one scalar value of `field` — the whole field
242
366
  * value, or one element of a scalar array (then `required` is false: element presence is not
@@ -244,6 +368,7 @@ function checkType(subType: string, value: unknown): string | null {
244
368
  */
245
369
  function scalarErrors(field: MetaData, value: unknown, required: boolean, label: string): ValidationFailure[] {
246
370
  const errors: ValidationFailure[] = [];
371
+ if (field.subType === FIELD_SUBTYPE_DECIMAL) return decimalErrors(field, value, label);
247
372
  const typeError = checkType(field.subType, value);
248
373
  if (typeError !== null) {
249
374
  errors.push({
@@ -256,8 +381,7 @@ function scalarErrors(field: MetaData, value: unknown, required: boolean, label:
256
381
  return errors;
257
382
  }
258
383
 
259
- const maxLen = resolveMaxLength(field);
260
- const minLen = resolveMinLength(field);
384
+ const { min: minLen, max: maxLen } = lengthBounds(field);
261
385
  if (typeof value === "string") {
262
386
  if (maxLen !== undefined && value.length > maxLen) {
263
387
  errors.push({
@@ -268,11 +392,11 @@ function scalarErrors(field: MetaData, value: unknown, required: boolean, label:
268
392
  received: value.length,
269
393
  });
270
394
  }
271
- // FR-036 Pin 1: a @required string is non-empty. The effective floor is
272
- // max(@min, 1) so the runtime OM rejects "" for a required string exactly as
273
- // the generated Zod InsertSchema (.min(1)) does — the two enforcement surfaces
274
- // stay in lockstep. A non-required field keeps its authored @min.
275
- const effectiveMin = Math.max(minLen ?? 0, required ? 1 : 0);
395
+ // FR-036 Pin 1: a @required string is non-empty by default (an implicit floor of 1),
396
+ // but an explicitly authored `validator.length @min` is ALWAYS authoritative over that
397
+ // floor (#224 / ADR-0044): `@min: 0` opts back to presence-only. Same rule as the
398
+ // generated Zod InsertSchema, so the two enforcement surfaces stay in lockstep.
399
+ const effectiveMin = minLen !== undefined ? minLen : required ? 1 : 0;
276
400
  if (effectiveMin > 0 && value.length < effectiveMin) {
277
401
  errors.push({
278
402
  field: label,
@@ -316,5 +440,44 @@ function scalarErrors(field: MetaData, value: unknown, required: boolean, label:
316
440
  });
317
441
  }
318
442
  }
443
+
444
+ // validator.numeric @min/@max — inclusive value bounds on a numeric field.
445
+ if (NUMERIC_FIELD_SUBTYPES.has(field.subType) || INT64_FIELD_SUBTYPES.has(field.subType)) {
446
+ const num = comparable(value);
447
+ const { min, max } = validatorBounds(field, VALIDATOR_SUBTYPE_NUMERIC);
448
+ if (num !== undefined && min !== undefined && num < min) {
449
+ errors.push({
450
+ field: label, rule: "numeric",
451
+ message: `'${label}' must be at least ${min} (got ${value})`,
452
+ expected: { min }, received: value,
453
+ });
454
+ }
455
+ if (num !== undefined && max !== undefined && num > max) {
456
+ errors.push({
457
+ field: label, rule: "numeric",
458
+ message: `'${label}' must be at most ${max} (got ${value})`,
459
+ expected: { max }, received: value,
460
+ });
461
+ }
462
+ }
463
+
464
+ // field.uri / field.inet — the strict format contract, unless @lenient opts out.
465
+ // ADR-0039: effective — @lenient may be inherited via extends.
466
+ if (typeof value === "string" && field.attr(FIELD_ATTR_LENIENT) !== true) {
467
+ if (field.subType === FIELD_SUBTYPE_URI && !isAbsoluteUri(value)) {
468
+ errors.push({
469
+ field: label, rule: "format",
470
+ message: `'${label}' must be an absolute URI`,
471
+ expected: "uri", received: value,
472
+ });
473
+ }
474
+ if (field.subType === FIELD_SUBTYPE_INET && !isInetLiteral(value)) {
475
+ errors.push({
476
+ field: label, rule: "format",
477
+ message: `'${label}' must be an IPv4 or IPv6 address`,
478
+ expected: "inet", received: value,
479
+ });
480
+ }
481
+ }
319
482
  return errors;
320
483
  }