@msw/data

repository·main·Indexed 21 days ago

https://github.com/mswjs/data

A data querying library for testing JavaScript applications that allows developers to create schema-based fixtures and query them using an ORM-inspired syntax. It supports runtime and type safety via the Standard Schema specification (Zod, ArkType, Valibot, or Yup), provides a query builder for filtering, and supports complex relational mapping including one-to-one, one-to-many, and many-to-many relationships.

Tokens
13.3K
Snippets
49
Records
60
Agent score
75%

What's inside @msw/data

  1. Define models using Standard Schema compatible libraries

    main

    Instead of a custom modeling syntax, @msw/data uses the Standard Schema specification. This allows you to use any standard-compliant modeling library (like Zod) to define the schema for a Collection. When creating a new Collection, pass your schema to the schema option.

    import { Collection } from '@msw/data'
    import { z } from 'zod'
    
    const users = new Collection({
      schema: z.object({
        id: z.number(),
        name: z.string().optional(),
      }),
    })
  2. Handle default values and nullability in v1

    main

    The primaryKey, nullable, and manual default value configurations from v0 are deprecated.

    1. Primary Keys: You no longer need to provide primary keys; you can query records by any property.
    2. Nullability: Use your schema library's native way to describe nullable properties.
    3. Default Values: Use your schema library's syntax to define defaults. These will be applied when calling .create().
    const users = new Collection({
      schema: z.object({
        subscribed: z.boolean().default(false)
      })
    })
    
    await users.create() // Result: { subscribed: false }
  3. Understand how primary keys work in @msw/data

    main

    The @msw/data library no longer uses an explicit primaryKey() function. Instead, every record is automatically assigned a random, immutable UUID upon creation.

    These internal IDs are used by the library to manage relationships between records. While you can treat any property of your model as a logical primary key for your own application logic, the library does not provide built-in functionality to enforce uniqueness or handle lookups based on custom primary keys.

  4. Define relations on a collection

    main

    You can define relationships between collections using the .defineRelations() method. This method allows you to specify how records in one collection relate to records in another (e.g., one-to-one, one-to-many, many-to-many).

    Key Principles:

    • Schema Level: You must first describe relational properties in your schema (e.g., using Zod getters) so that the types are aware of the relations. .defineRelations() only handles the runtime logic and does not affect the model's types.
    • Order of Definition: Relations must be defined after the collections are instantiated to prevent circular reference issues during setup.
    • Internal IDs: Relations are bound to the internal IDs of related records; you do not need to manage explicit foreignKey associations.

    Relation Types:

    • One-to-one: Use the one(targetCollection) helper.
    • One-to-many: Use the many(targetCollection) helper.
    • Many-to-many: Define many relations on both participating collections.
    • Polymorphic: Pass an array of collections to the many() helper to allow a relation to reference multiple different collection types.
    const users = new Collection({ schema: userSchema });
    const countries = new Collection({ schema: countrySchema });
    
    users.defineRelations(({ one }) => ({
      country: one(countries),
    }));
  5. Query records using the query builder

    main

    To target specific records, construct a query using a predicate function. The query builder q allows you to filter records based on property values or custom logic.

    Basic equality and function predicates

    You can match exact values or use a function to return a boolean for more complex logic (e.g., startsWith).

    Nested queries

    Query functions work at any level of nesting, including checking properties of related objects.

    Logical operators

    Use .or() and .and() to combine predicates. You can use method chaining or functional composition.

    // Basic equality
    users.findFirst((q) => q.where({ name: 'John' }))
    
    // Function predicate
    users.findFirst((q) => q.where({ name: (name) => name.startsWith('John') }))
    
    // Nested property check
    users.findFirst((q) => q.where((user) => user.posts.length > 0))
    
    // Logical OR (method chaining)
    users.findMany((q) =>
      q.where({ posts: (posts) => posts.length > 0 }).or({ role: 'editor' }),
    )
    
    // Logical OR (functional composition)
    users.findMany((q) =>
      q.or(
        q.where({ posts: (posts) => posts.length > 0 }),
        q.where({ role: 'editor' }),
      ),
    )
  6. Use the query builder pattern for complex queries

    main

    Instead of using a nested object syntax with logical keys like OR or AND mixed with record properties, @msw/data uses a query builder pattern. This pattern uses a callback function that provides a query object (q) to compose predicates. This approach keeps record properties separate from logical expressions, making queries more readable and composable.

    To build a query, use the callback pattern with methods like .where(), .or(), and .and() to wrap your predicates.

    // Using the query builder pattern for composition
    users.findFirst((q) =>
      q.or(q.where({ id: 2 }), q.where({ name: 'Bob' }))
    )
  7. Create a data collection

    main

    A Collection is the primary unit of data in @msw/data. You define it by providing a schema that follows the Standard Schema specification. This allows you to use libraries like Zod, ArkType, Valibot, or Yup to describe your data models with full runtime and type safety.

    import { Collection } from '@msw/data'
    import { z } from 'zod'
    
    const users = new Collection({
      schema: z.object({
        id: z.number(),
        name: z.string(),
      }),
    })
  8. Perform collocated updates on relations

    main

    In v1, you can update an owner and its related foreign records in a single operation. By modifying the foreign record's values within the data callback of an update call, the library automatically handles the implicit update of the reference.

    const posts = new Collection({ schema: postSchema })
    const revisions = new Collection({ schema: revisionSchema })
    
    posts.defineRelations(({ one }) => ({
      revision: one(revisions)
    }))
    
    await posts.update(q => q.where({ id: 'post-1' }), {
      data(post) {
        post.title = 'Renamed post'
        // Updating the related record's value triggers an implicit update
        post.revision.updatedAt = Date.now()
      }
    })
  9. Implement One-to-many and Inversed relations

    main

    A one-to-many relation allows one record to be associated with multiple records in another collection. If you define the relation on both sides (inversed), the library automatically synchronizes them.

    One-to-many

    posts.defineRelations(({ many }) => ({
      comments: many(comments),
    }))

    Inversed (Two-way) relations

    When both collections define relations to each other, updating one side automatically updates the other. For example, adding a comment to a post will automatically set the post property on that comment.

    posts.defineRelations(({ many }) => ({
      comments: many(comments),
    }))
    
    comments.defineRelations(({ one }) => ({
      post: one(posts),
    }))
    const postSchema = z.object({
      get comments() {
        return z.array(commentSchema)
      },
    })
    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),
    }))
  10. Define custom logic using function predicates

    main

    Rather than using convenience keys (like in or equals), @msw/data allows you to use function predicates for maximum flexibility. You can define custom logic in two ways:

    1. Field-level predicates: Pass a function to a specific field to describe how that field should be matched.
    2. Record-level predicates: Pass a function to .where() that accepts the entire record as an argument, allowing for complex logic involving multiple fields or external state.

    This removes the need for specialized library keys and allows for infinitely more powerful matching logic.

    // Field-level predicate (replaces 'in' syntax)
    users.findFirst((q) =>
      q.where({
        id: (id) => isList.includes(id),
      }),
    )
    
    // Record-level predicate (custom logic on the whole object)
    users.findFirst((q) =>
      q.where((user) => {
        return hasRole('admin', user)
      }),
    )
  11. Migrate from v0 to v1

    main

    Version 1.0 is a major overhaul of @msw/data. When migrating from v0.x.x, be aware of the following terminology changes:

    • Model is now Collection
    • Entry is now Record
    • Relationship is now Relation

    Additionally, the package name has changed from @mswjs/data to @msw/data.

    # Old package
    npm i @mswjs/data
    
    # New package
    npm i @msw/data