Data
Data querying library for testing JavaScript applications.
Install / Use
/learn @mswjs/DataREADME
Motivation
This library exists to help developers model and query data when testing and developing their applications. It acts as a convenient way of creating schema-based fixtures and querying them with a familiar ORM-inspired syntax. It can be used standalone or in conjuncture with Mock Service Worker for seamless mocking experience both on the network and the data layers.
Features
- Relies on Standard Schema instead of inventing a proprietary modeling syntax you have to learn. You can use any Standard Schema-compliant object modeling library to describe your data, like Zod, ArkType, Valibot, yup, and many others.
- Full runtime and type-safety.
- Provides a powerful querying syntax (inspired by Prisma);
- Supports relations for database-like behaviors (inspired by Drizzle);
- Supports extensions (including custom extensions) for things like cross-tab collection synchronization or record persistence.
Getting started
Install
npm i @msw/data --save-dev
Create collection
You start by defining a data collection.
import { Collection } from '@msw/data'
import { z } from 'zod'
const users = new Collection({
schema: z.object({
id: z.number(),
name: z.string(),
}),
})
Above, I'm using Zod to describe the
schemafor my users collection. You can use whichever Standard Schema compliant library of your choice instead.
Seed collection
Next, let's put some values into our collection. Those values are called records and you can create individual records via the .create() method or create a bunch of them with .createMany().
await users.create({ id: 1, name: 'John' })
await users.createMany(5, (index) => ({
id: index + 1,
name: faker.person.firstName(),
}))
Combine
.createMany()with tools like Faker for random values in your records.
Use collection
From this point on, you can use your users collection for anything data-related. You can create more records, query them, define relations to other collections, update and delete records, etc. Learn more you can do with the library in the documentation below. Good luck!
Querying
Query syntax
Whenever you have to target a record(s), you construct a query. A query acts as a predicate that a record must match in order to be targeted. The most basic query is that describing expected values of the record's properties:
users.findFirst((q) => q.where({ name: 'John' }))
Above, we are defining a query using the
qbuilder that targets the first user whosenameproperty equals to'John'.
Additionally, any property in a query can be expanded into a function that accepts the value and returns a boolean, indicating whether the record matches:
users.findFirst((q) =>
q.where({
name: (name) => name.startsWith('John'),
}),
)
This query matches the first user whose
namestarts with'John'. Use functions as predicates to express more advanced logic in your queries.
Query functions are supported at any level of nesting, including the top-level record itself.
users.findFirst((q) => q.where((user) => user.posts.length > 0))
users.findFirst((q) =>
q.where({
address: {
street: (street) => street !== 'Baker st.',
},
}),
)
Logical operators
You can build complex queries via .or() and .and() logical operators exposed through the query builder. For example, here's a query that matches all users who have posts or are editors:
users.findMany((q) =>
q.where({ posts: (posts) => posts.length > 0 }).or({ role: 'editor' }),
)
If you prefer functional composition over method chaining, you can wrap predicates in q.or() and q.and() instead. Both syntaxes result in the same query.
users.findMany((q) =>
q.or(
q.where({ posts: (posts) => posts.length > 0 }),
q.where({ role: 'editor' }),
),
)
Pagination
This library supports offset and cursor-based pagination.
Offset-based pagination
Provide the take property onto the options object of any bulk operation method, like .findMany(), .updateMany(), or .deleteMany(), to limit the number of results returned by the query.
const users = new Collection({ schema })
users.findMany(
(q) => q.where({ email: (email) => email.includes('@google.com') }),
{
// Return the first 5 matching records.
take: 5,
},
)
You can also skip the number of first matching results by providing the skip property:
const users = new Collection({ schema })
users.findMany(
(q) => q.where({ email: (email) => email.includes('@google.com') }),
{
// Skip the first 10 matching records.
skip: 10,
// And return the next 5.
take: 5,
},
)
The negative value for take is also supported to have backward pagination:
users.findMany(
(q) => q.where({ email: (email) => email.includes('@google.com') }),
{
take: -5,
},
)
Cursor-based pagination
Provide a reference to the record of the same collection as the cursor property for cursor-based pagination.
const users = new Collection({ schema })
const john = users.findFirst((q) => q.where({ name: 'John' }))
users.findMany((q) => q.where({ subscribed: true }), {
cursor: john,
take: 5,
})
Sorting
You can sort the results of bulk operations, like .findMany(), .updateMany(), and .deleteMany(), by providing the orderBy property in that operation's options.
// Find all users whose name starts with "J"
// and return them sorted by their `name`.
users.findMany((q) => q.where({ name: (name) => name.startsWith('J') }), {
orderBy: { name: 'asc' },
})
You can sort by multiple criteria by providing them in the orderBy object:
users.updateMany((q) => q.where({ name: (name) => name.startsWith('J') }), {
data(user) {
user.name = user.name.toUpperCase(),
},
orderBy: {
name: 'asc',
id: 'desc',
},
})
Relations
You can define relations by calling the .defineRelations() method on the collection.
- One-to-one
- One-to-many
- One-to-many (inversed)
- Many-to-many
- Through relations
- Unique relations
- Ambiguous relations
- Polymorphic relations
Defining relations
Below, you can find examples of defining various types of relations, but there are a few things that apply to all of them:
- Describe relations on the schema level using your schema library. The
.defineRelations()API has no effect on the model's schema/types and only operates on known properties; - Relations are described after a collection is defined (to prevent circular references);
- Relations do not require explicit
foreignKeyassociations and instead are bound to internal IDs of related records.
One-to-one
const userSchema = z.object({
// In Zod, relational properties are best described as getters
// so they can produce self-referencing schemas.
get country() {
return countrySchema
},
})
const countrySchema = z.object({ code: z.string() })
const users = new Collection({ schema: userSchema })
const countries = new Collection({ schema: countrySchema })
// Declare the relations on the `users` collection.
users.defineRelations(({ one }) => ({
// `user.country` is a one-of relation to the `countries` collection.
country: one(countries),
}))
const user = await users.create({
country: await countries.create({ code: 'usa' }),
})
user.country // { code: 'usa' }
One-to-many
const postSchema = z.object({
get comments() {
return z.array(countrySchema)
},
})
const commentSchema = z.object({
text: z.string(),
})
const posts = new Collection({ schema: postSchema })
const comments = new Collection({ schema: commentSchema })
posts.defineRelations(({ many }) => ({
comments: many(comments),
}))
await posts.create({
comments: [
await comments.create({ text: 'First!' }),
await comments.create({ text: 'Thanks for watching.' }),
],
})
One-to-many (inversed)
Two collections may self-reference each other. For example, post.comments is a list of comments while each comment.post references to the parent post.
const postSchema = z.object({
get comments() {
return z.array(countrySchema)
},
})
const commentSchema = z.object({
text: z.string(),
get post() {
return postSchema
},
})
const posts = new Collection({ schema: postSchema })
const comments = new Collection({ schema: commentSchema })
posts.defineRelations(({ many }) => ({
comments: many(comments),
}))
comments.defineRelations(({ one }) => ({
post: one(posts),
}))
await posts.create({
comments: [await comments.create({ text: 'First!' })],
})
const comment = comments.findFirst((q) => q.where({ text: 'First!' }))
comment.post // { comments: [{ text: 'First', post: Circular }] }
Inversed relations are updated automatically. Whenever you add a new comment to a post, both
post.commentsandcomment.postare updated to reference each other. The same is true when setting a new parentposton the comment.
Many-to-many
In the next example, every user may have multiple posts while each post may have multiple authors.
const userSchema = z.object({
get posts() {
return z.array(postSchemas)
},
})
const postSchema = z.object({
get authors() {
return z.array(userSchema)
},
})
const users = new Collection({ schema: userSchema })
const posts = new Collecti
Related Skills
gh-issues
353.3kFetch GitHub issues, spawn sub-agents to implement fixes and open PRs, then monitor and address PR review comments. Usage: /gh-issues [owner/repo] [--label bug] [--limit 5] [--milestone v1.0] [--assignee @me] [--fork user/repo] [--watch] [--interval 5] [--reviews-only] [--cron] [--dry-run] [--model glm-5] [--notify-channel -1002381931352]
node-connect
353.3kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
oracle
353.3kBest practices for using the oracle CLI (prompt + file bundling, engines, sessions, and file attachment patterns).
taskflow-inbox-triage
353.3kname: taskflow-inbox-triage description: Example TaskFlow authoring pattern for inbox triage. Use when messages need different treatment based on intent, with some routes notifying immediately, some w
