
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
normalized-cache
Advanced tools
This normalized cache provides the following functionality:
The library is around 6 KB gzipped.
Installation:
npm install --save normalized-cache
import { Cache, schema } from "normalized-cache";
const Author = schema.object({
name: "Author",
});
const Post = schema.object({
name: "Post",
fields: {
author: Author,
},
});
const cache = new Cache({
types: [Post],
});
cache.write({
type: "Post",
data: {
id: "1",
title: "Title",
author: {
id: "2",
name: "Name",
},
},
});
const { data } = cache.read({
type: "Post",
id: "1",
});
const { data } = cache.read({
type: "Author",
id: "2",
});
class Cache {
get(entityID: string, optimistic?: boolean): Entity | undefined;
set(entity: Entity, optimistic?: boolean): Entity;
identify(options: IdentifyOptions): string | undefined;
read(options: ReadOptions): ReadResult;
write(options: WriteOptions): WriteResult;
delete(options: DeleteOptions): DeleteResult;
invalidate(options: InvalidateOptions): InvalidateResult;
watch(options: WatchOptions): Unsubscribable;
silent(fn: () => void): void;
transaction(fn: () => void): void;
reset(): void;
gc(): void;
retain(entityID: string): Disposable;
addOptimisticUpdate(updateFn: OptimisticUpdateFn): OptimisticUpdateDisposable;
removeOptimisticUpdate(id: number): void;
}
const schema = {
array(config?: ArrayTypeConfig | ValueType): ArrayType
boolean(config?: BooleanTypeConfig): BooleanType
nonNullable(config: NonNullableTypeConfig | ValueType): NonNullableType
number(config?: NumberTypeConfig): NumberType
object(config?: ObjectTypeConfig): ObjectType
string(config?: StringTypeConfig | string): StringType
union(config: UnionTypeConfig | ValueType[]): UnionType
}
Schema types allow you to define entities, relationships and fields.
Learn more about the type system here.
When writing to the cache, a type must be provided.
cache.write({
type: "Post",
data: { id: "1", title: "Title" },
});
A ID can be specified if this cannot be inferred from the data itself:
cache.write({
type: "Post",
id: "1",
data: { title: "Title" },
});
If the ID is an object or array it will be automatically serialized to a stable string:
cache.write({
type: "Posts",
id: { page: 1, limit: 10 },
data: [],
});
Reading from the cache can be done with the read method.
When no selector is given, all data related to the entity will be returned:
cache.write({
type: "Author",
data: {
id: "2",
name: "Author",
},
});
cache.write({
type: "Post",
data: {
id: "1",
title: "Title",
author: {
id: "2",
},
},
});
const { data } = cache.read({
type: "Post",
id: "1",
});
console.log(data);
// {
// id: "1",
// title: "Title",
// author: {
// id: "2",
// name: "Author",
// },
// }
The resulting data can contain circular references when entities refer to each other.
Selectors can be used to select specific fields:
import { cql } from "normalized-cache";
cache.write({
type: "Author",
data: {
id: "2",
name: "Author",
},
});
cache.write({
type: "Post",
data: {
id: "1",
title: "Title",
author: {
id: "2",
},
},
});
const { data } = cache.read({
type: "Post",
id: "1",
select: cql`{ title author { name } }`,
});
console.log(data);
// {
// title: "Title",
// author: {
// name: "Author",
// },
// }
Learn more about selectors here.
The write method also returns a selector that matches the exact shape of the input:
cache.write({
type: "Author",
data: {
id: "2",
name: "Author",
},
});
const { selector } = cache.write({
type: "Post",
data: {
id: "1",
title: "Title",
author: {
id: "2",
},
},
});
const { data } = cache.read({
type: "Post",
id: "1",
select: selector,
});
console.log(data);
// {
// id: "1",
// title: "Title",
// author: {
// id: "2",
// },
// }
Computed fields can be created by defining a field with a read function.
Defining a computed field for calculations:
const Cart = schema.object({
name: "Cart",
fields: {
totalPrice: {
read: (cart) => {
return cart.items.reduce((total, item) => total + item.price, 0);
},
},
},
});
Defining a relational field based on another field:
const Author = schema.object({
name: "Author",
});
const Post = schema.object({
name: "Post",
fields: {
author: {
read: (post, { toReference }) => {
return toReference({ type: "Author", id: post.authorId });
},
},
},
});
Fields that do not match with the schema will be reported in the invalidFields array:
const LoggedIn = schema.boolean({ name: "LoggedIn" })
const cache = new Cache({ types: [LoggedIn] });
cache.write({ type: "LoggedIn" data: "string" });
const { invalidFields } = cache.read({ type: "LoggedIn" });
if (invalidFields) {
console.log("Invalid data");
}
Fields that are missing will be reported in the missingFields array:
const LoggedIn = schema.boolean({ name: "LoggedIn" });
const cache = new Cache({ types: [LoggedIn] });
const { missingFields } = cache.read({ type: "LoggedIn" });
if (missingFields) {
console.log("Missing data");
}
The stale flag indicates if some entity or field has been invalidated or if any expiresAt has past:
const LoggedIn = schema.boolean({ name: "LoggedIn" });
const cache = new Cache({ types: [LoggedIn] });
cache.write({ type: "LoggedIn" data: true, expiresAt: 0 });
const { stale } = cache.read({ type: "LoggedIn" });
if (stale) {
console.log("Stale data");
}
Data in the cache can be watched with the watch method.
Watching for any change in a specific post and all related data:
const { unsubscribe } = cache.watch({
type: "Post",
id: "1",
callback: (result, prevResult) => {
// log
},
});
unsubscribe();
Watching specific fields:
cache.watch({
type: "Post",
id: "1",
select: cql`{ title }`,
callback: (result, prevResult) => {
if (!prevResult.stale && result.stale) {
// The title became stale
}
},
});
Entities and fields can be invalidated with the invalidate method.
When an entity or field is invalidated, all related watchers will be notified.
Invalidate an entity:
cache.invalidate({
type: "Post",
id: "1",
});
Invalidate entity fields:
cache.invalidate({
type: "Post",
id: "1",
select: cql`{ comments }`,
});
when expiresAt is specified, all affected fields will be considered stale after the given time:
cache.write({
type: "Post",
data: { id: "1" },
expiresAt: Date.now() + 60 * 1000,
});
Set expiration for certain types:
cache.write({
type: "Post",
data: { id: "1" },
expiresAt: {
Comment: Date.now() + 60 * 1000,
},
});
Entities and fields can be deleted with the delete method.
Deleting an entity:
cache.delete({
type: "Post",
id: "1",
});
Deleting specific fields:
cache.delete({
type: "Post",
id: "1",
select: cql`{ title }`,
});
An optimistic update function can be used to update the cache optimistically.
These functions will be executed everytime the cache is updated, until they are removed.
This means that if new data is written to the cache, the optimistic update will be re-applied / rebased on top of the new data.
async function addComment(postID, text) {
function addCommentToPost(comment) {
const { data } = cache.read({
type: "Post",
id: postID,
select: cql`{ comments }`,
});
cache.write({
type: "Post",
id: postID,
data: { comments: [...data.comments, comment] },
});
}
const { dispose } = cache.addOptimisticUpdate(() => {
const optimisticComment = { id: uuid(), text };
addCommentToPost(optimisticComment);
});
const comment = await api.addComment(postID, text);
cache.transaction(() => {
dispose();
addCommentToPost(comment);
});
}
By default entities are shallowly merged and non-entity values are replaced.
This behavior can be customized by defining custom write functions on entities and fields.
Replacing entities instead of merging:
const Author = schema.object({
name: "Author",
write: (incoming) => {
return incoming;
},
});
Merging objects instead of replacing:
const Post = schema.object({
name: "Post",
fields: {
content: {
type: schema.object(),
write: (incoming, existing) => {
return { ...existing, ...incoming };
},
},
},
});
Transforming values when writing:
const Post = schema.object({
name: "Post",
fields: {
title: {
write: (incoming) => {
if (typeof incoming === "string") {
return incoming.toUpperCase();
}
},
},
},
});
Multiple changes can be wrapped in a transaction to make sure watchers are only notified once after the last change:
cache.transaction(() => {
cache.write({ type: "Post", data: { id: "1", title: "1" } });
cache.write({ type: "Post", data: { id: "2", title: "2" } });
});
Wrap changes with silent to prevent watchers from being notified:
cache.silent(() => {
cache.write({ type: "Post", data: { id: "1", title: "1" } });
});
The gc method can be used to remove all unwatched and unreachable entities from the cache.
Use the retain method to prevent an entity from being removed.
FAQs
A cache for storing normalized data.
We found that normalized-cache demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.