GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — feat: add `check`, `restrictQuery`, `skipUnchanged`, `counterCache`, `denormalize` and `getChanges` by fratzinger · Pull Request #68 · feathersjs/feathers-utils

Conversation

@fratzinger

@fratzinger fratzinger commented Oct 5, 2026

Copy link
Copy Markdown
Member

Builds on #67 (getMergedData, generated, unique, the shared fetches), so this PR targets its branch. Once #67 is merged, it needs a rebase onto main.

New

Hooks

  • check works like a CHECK constraint across fields, e.g. startDate <= endDate. The fields are read with getMergedData, so a patch of only endDate is checked against the stored startDate, which a validator of data can't do. Nothing is checked for a patch that touches none of the fields, or for items whose fields keep their values. test runs per item and may be async; error replaces the BadRequest.
  • restrictQuery({ fields }) limits the fields external callers may filter, sort and select by, and the operators they may use on each. It covers $and/$or/$nor, $sort, $select and nested paths, and the id field is always allowed. Without it, a client can filter by { password: { $regex: '^a' } }. Internal calls are not restricted.
  • skipUnchanged drops the fields of a patch that keep their stored value (for every item of a multi patch). If nothing changes, the write is skipped: the result is the stored items, with the call's $select, and no patched event is sent. ignore: ['updatedAt'] keeps timestamps from counting as a change.
  • counterCache keeps a count on the parent item, e.g. posts.commentCount, across create, remove, and patch/update that moves items. It counts every touched parent again with count (where can narrow the count) instead of $inc, which isn't portable. Parents with an equal count share a write.
  • denormalize copies fields from a related item and keeps them in sync. It returns two hooks:
    • target is a generated hook that fills e.g. posts.authorName from users.name, with one query per call.
    • source copies changed fields to the target items with one patchMany per changed source item. These patches go through the target's hooks and send events. The two hooks share a mark in params, so target lets the patches of source through.

Utils

  • getChanges(before, after, fields?) returns the fields that differ, as { [path]: { before, after } }, compared with isDeepEqual.

Options

  • select: false on getMergedData, generated, unique and check fetches whole items instead of just the needed fields, so stash, trigger and later hooks of the call reuse the fetch.

Changed

  • preventChanges now compares with the stored item:
    • On update it always compares. A changed field gets its stored value back (or the hook throws with error). Before, it deleted the field, because update replaces the whole item, and with error it always threw.
    • On patch, the comparison is opt-in with compare: true. A nested field that changes gets its stored value back instead of being removed, since its parent replaces the stored one as a whole.

Internal

  • call-fetches.ts: fetchStored (the items before the call, narrow or whole, shared per call) and resultWith (the result, refetched only if the call's $select drops a needed field). Used by getMergedData, preventChanges, skipUnchanged, counterCache and denormalize.

Review notes

  • preventChanges on update is a behavior change, see above.
  • preventChanges without compare still turns { role: { type } } into { role: {} } when role.type is protected, which wipes the stored role.type. This is existing behavior with an existing test, so I left it unchanged; it's worth a follow-up.
  • counterCache and denormalize write after the call. Without a transaction in params, those writes are not atomic with the call.

Testing

pnpm test (lint, typecheck, coverage) passes: 173 test files, 2091 tests, coverage 97.8%. pnpm build passes, and the new exports load from dist.

🤖 Generated with Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@fratzinger @claude
feat: add check, restrictQuery, skipUnchanged, counterCache, …
ed44b7f
…`denormalize` and `getChanges`

New:
- hooks: `check` (a CHECK constraint across fields), `restrictQuery`
  (fields and operators external callers may query by), `skipUnchanged`
  (drops unchanged patch fields, skips no-op writes), `counterCache` (a count
  on the parent item) and `denormalize` (copied fields, kept in sync)
- utils: `getChanges`
- `getMergedData`, `generated`, `unique` and `check` take `select: false`
  to fetch whole items, which later hooks of the call reuse

Changed:
- `preventChanges` compares with the stored item on `update` - and restores
  a changed field instead of deleting it - and on `patch` with `compare`

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 5, 2026

Copy link
Copy Markdown

Deploying feathers-utils with  Cloudflare Pages  Cloudflare Pages

Latest commit: ed44b7f
Status: ✅  Deploy successful!
Preview URL: https://dcd59e29.feathers-utils.pages.dev
Branch Preview URL: https://feat-check-restrict-query-de.feathers-utils.pages.dev

View logs

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@fratzinger

fratzinger commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

Superseded by #67, which now contains these changes (with aggregate in place of counterCache).

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@fratzinger fratzinger closed this Oct 5, 2026
@fratzinger
fratzinger deleted the feat/check-restrict-query-denormalize branch October 5, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

1 participant

@fratzinger
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.