npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xeplr/schema-handler

v1.0.8

Published

Field-schema primitive: define, validate, infer, map, and template values across schemas

Readme

@xeplr/schema-handler

A field schema, and what to do with one. A schema is a plain array of field definitions — { name, type, required, default, options, validation }. This package checks a values object against it (filling defaults, collecting every problem into one ValidationError), infers a schema from a live object, resolves dotted-path mappings, and fills {placeholder} templates.

It has no dependencies and no I/O, so the same schema can be checked in the browser and on the server. @xeplr/factory validates every record save through applySchema with the screen's own fields, and @xeplr/actions validates an action's input the same way — so the server accepts exactly what the form does.

Install

npm i @xeplr/schema-handler

No dependencies, no peers. CommonJS (see CommonJS / ESM).

Quick start

var { applySchema, ValidationError } = require('@xeplr/schema-handler')

var schema = [
  { name: 'title',    type: 'string', required: true, validation: { maxLength: 10 } },
  { name: 'priority', type: 'string', options: ['low', 'high'], default: 'low' },
  { name: 'qty',      type: 'number', validation: { min: 1, integer: true } }
]

applySchema(schema, { title: 'Plant', qty: 3, extra: 1 })
// → { title: 'Plant', priority: 'low', qty: 3 }        default filled, `extra` dropped

try {
  applySchema(schema, { title: 'Plant seeds now', qty: 1.5, priority: 'mid' }, 'task')
} catch (err) {
  err instanceof ValidationError   // true
  err.details
  // [ { field: 'title',    message: 'task field "title" must be at most 10 characters' },
  //   { field: 'priority', message: 'task field "priority" must be one of: low, high' },
  //   { field: 'qty',      message: 'task field "qty" must be an integer' } ]
}

API

| export | signature | does | |---|---|---| | applySchema | (schema, values, label?) → object | Validates values against schema; returns a new object with defaults filled. Throws ValidationError listing every problem. | | ValidationError | class extends Error — new ValidationError(message, details?) | name: 'ValidationError', details: [{ field, message }]. | | TYPES | string[] | ['string', 'number', 'boolean', 'date', 'object', 'array'] | | CHECKERS | { [type]: (value) → boolean } | The runtime predicate for each type in TYPES. | | check | (type, value) → boolean | Runs CHECKERS[type]; an unknown type returns true. | | inferType | (value) → string | 'null' (null/undefined), 'array', 'date' (a Date), else typeof value. | | schemaFromObject | (obj) → { [key]: { type } } | One entry per own key, typed with inferType. Non-objects give {}. | | mergeSchemas | (current, fresh) → object | Copy of current plus keys of fresh that current lacks. Existing keys are never overwritten. | | schemasEqual | (a, b) → boolean | JSON.stringify(a \|\| {}) === JSON.stringify(b \|\| {}) — key order matters. | | getPath | (obj, path) → any | Reads a dotted path ('user.tags.1'); undefined if any step is missing. | | resolveMapping | (mapping, source) → object | { target: 'dotted.path' } → { target: value }. Missing paths give undefined. | | interpolate | (template, context) → string | Replaces {path} with getPath(context, path). | | interpolateAll | (value, context) → any | interpolate on every string inside objects and arrays; other values returned as-is. | | resolve | (template, context) → any | A template that is exactly one {path} gives the value with its type (a list stays a list, a number a number; null when missing). Anything with text around it is interpolate's text. | | resolveAll | (value, context) → any | resolve through objects and arrays. What a workflow step hands the next one. | | formatDate | (value, 'FORMAT[:ZONE]') → string | The date pipe on its own. |

Note that schemaFromObject / mergeSchemas / schemasEqual work on a keyed schema ({ key: { type } }), not the field array applySchema takes. They exist for "learn missing fields" and "dynamic" output schemas.

The schema

applySchema takes an array of fields. Anything that is not an array is not a schema: applySchema(null, values) returns a shallow copy of values, unchecked.

| key | | | |---|---|---| | name | required | The key in values. A field without a name is skipped. | | type | optional | One of TYPES. An unknown type, or none, accepts any value. | | required | optional | Missing (undefined or null) with no default is an error. | | default | optional | Used when the value is missing. Not type-checked or validated. Wins over required. | | options | optional | [value, …] or [{ value, label }, …]. The value must be one of them (strict ===). | | validation | optional | Per-type constraints, below. | | description, order | ignored | Carried for forms; not read here. | | group, showWhen | ignored | Form layout only — see Rules. |

Types

| type | accepts | |---|---| | string | typeof v === 'string' | | number | typeof v === 'number' and not NaN — the string '3' is not a number | | boolean | typeof v === 'boolean' | | date | a Date, or a string Date.parse understands | | object | non-null object that is not an array and not a Date | | array | Array.isArray(v) |

validation

Checked only after the value passed its type, and only for these types:

| type | keys | |---|---| | string | minLength, maxLength, pattern (a string passed to new RegExp) | | number | min, max, integer | | date | min, max — ISO strings, compared as Dates, inclusive | | array | minItems, maxItems, itemType (each item checked with check; errors keyed tags[1]) | | object | fields: [...] — a nested schema, applied recursively |

A nested object's result is the nested applySchema output (so its defaults are filled and its unknown keys dropped). Its errors are re-keyed parent.child:

applySchema(
  [{ name: 'address', type: 'object', validation: { fields: [{ name: 'city', type: 'string', required: true }] } }],
  { address: {} }, 'task')
// throws; details: [ { field: 'address.city', message: 'Missing required task.address field: city' } ]

What comes back

  • Only fields named in the schema. Keys not in the schema are dropped.
  • A missing value with a default → the default. A missing value without one → the key is absent.
  • An empty string is a value, not missing.

Errors

applySchema checks every field, then throws one ValidationError if anything failed:

| property | | |---|---| | name | 'ValidationError' | | message | every message joined with '; ' — for logs | | details | [{ field, message }], one per problem — for field-level UI |

Per field, the first failing stage wins: missing → type → options → constraints. A type error stops that field's other checks; several constraint failures on one field are all reported.

The label argument (default 'field') goes into every message:

| problem | message | |---|---| | required, missing | Missing required <label> field: <name> | | wrong type | <label> field "<name>" expected type <type>, got <typeof value> | | not an option | <label> field "<name>" must be one of: a, b | | minLength / maxLength | … must be at least N characters / … must be at most N characters | | pattern | … does not match the required pattern | | min / max (number) | … must be >= N / … must be <= N | | integer | … must be an integer | | min / max (date) | … must be on or after <min> / … must be on or before <max> | | minItems / maxItems | … must have at least N items / … must have at most N items | | itemType | <label> field "<name>[i]" expected type <itemType> |

With the default label messages read Missing required field field: title; callers pass their own ('input', 'config', 'params').

Not a ValidationError: an invalid validation.pattern makes new RegExp throw its own SyntaxError.

details is shaped so it can be turned straight into @xeplr/ui-schema-handler's DynamicForm errors prop ({ [name]: message }) without parsing a field name back out of a sentence.

Mapping and templating

var { resolveMapping, interpolate, interpolateAll } = require('@xeplr/schema-handler')

resolveMapping({ who: 'user.name', age: 'user.age' }, { user: { name: 'Ann' } })
// → { who: 'Ann', age: undefined }

interpolate('Hi {user.name}, {{literal}} {missing}! {user}', { user: { name: 'Ann' } })
// → 'Hi Ann, {literal} ! {"name":"Ann"}'

interpolateAll({ subject: 'Hi {name}', n: 3, list: ['{name}', true] }, { name: 'Ann' })
// → { subject: 'Hi Ann', n: 3, list: ['Ann', true] }

| in a template | renders | |---|---| | {a.b.c} | the value at that dotted path (whitespace inside the braces trimmed) | | a path that is undefined / null | '' | | an object or array | JSON.stringify(value) | | {{ / }} | a literal { / } | | template is null / undefined | '' |

Paths are split on . only, so array items are items.0, not items[0].

Formatting a date: {path|date:FORMAT}

interpolate('Recon-{trigger.window.from|date:YYYY-MM:Asia/Kolkata}.xlsx', ctx)  // 'Recon-2026-09.xlsx'
interpolate('{params.month|date:MMM YYYY}', { params: { month: '2026-09-01' } })   // 'Sep 2026'
  • Tokens: YYYY YY MMMM (September) MMM (Sep) MM M DD D HH mm ss. Text in [brackets] is written as it is. No format means YYYY-MM-DD.
  • A date-only value (2026-09-01) is that calendar day, wherever the server is.
  • An instant (2026-08-31T18:30:00Z) is written in the zone named last (:Asia/Kolkata, :UTC), or else in the server's own time zone. Why: a job's period is stored in UTC, and September in India begins at 18:30 UTC on 31 August, which is August if written in UTC.
  • Refused, naming the problem (code: 'TEMPLATE_INVALID'): a value that is not a date, a zone that is not one, a pipe that does not exist ({x|dat}). A typo must not quietly write an empty file name.
  • Nothing to format is nothing: a missing value renders '', as any missing path does.
  • Braces that are not a path and a pipe (a JSON fragment) behave as before.

CommonJS / ESM

The package is CommonJS (main: index.js, no exports map).

var { applySchema } = require('@xeplr/schema-handler')

From ESM, use the default import. Node's CommonJS export detection cannot see this file's name: lib.fn properties, so named imports (other than TYPES) fail:

import schemaHandler from '@xeplr/schema-handler'
const { applySchema, ValidationError } = schemaHandler

Rules the code enforces

| rule | why | |---|---| | All problems are collected, then thrown once | A form can show every field's error at once instead of one per submit. | | details is [{ field, message }], message a joined string | Field-level UI needs the field; logs need one string. Neither should parse the other. | | group is ignored when validating | It only draws fields together in a collapsible section. A collapsed section's values are submitted and checked like any other — "did the user have this section open" must never be part of what the server accepts. | | showWhen is ignored when validating | Layout only: it decides whether a field is asked at all. Use showWhen for a branch, group for the tail of rarely-set options. | | options is the constraint | A radio/select's option list is not declared again as a separate rule — one source of truth rather than two that can drift. | | validation runs only after type passed | Constraints like .length or < are meaningless on the wrong type. | | Nested errors keyed parent.child | A flat details array still says exactly which leaf failed. | | Unknown type accepts the value | check does not reject what it does not know. |

Tests

npm test: test/templating.test.js (interpolation, typed resolve, the date pipe across zones). The rest is exercised through its consumers, e.g. @xeplr/factory's npm test.

License

MIT