diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 5a7e990c..2eecbe82 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,2 +1,2 @@
github: mswjs
-open_collective: mswjs
+open_collective: mswjs
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1d46414e..3ca48c44 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,32 +4,37 @@ on:
push:
branches: [main]
pull_request:
- branches: [main]
-
workflow_dispatch:
jobs:
- build:
+ test:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
+
+ - name: Set up pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9.15.0
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: 'pnpm'
- name: Install dependencies
- run: yarn install --frozen-lockfile
+ run: pnpm install
- - name: Environment (versions)
- run: |
- echo "node: $(node -v)"
- echo "npm: $(npm -v)"
- echo "typescript: $(npm ls typescript)"
- echo "tsc: $(tsc -v)"
+ - name: Install browsers
+ run: pnpm exec playwright install chromium --with-deps
- name: Build
- run: yarn build
+ run: pnpm build
- - name: Tests
- run: yarn test
+ - name: Tests (Node.js)
+ run: pnpm test:node
- - name: Tests (typings)
- run: yarn test:ts
+ - name: Tests (browser)
+ run: pnpm test:browser
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 538cf671..f6bc65f7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,29 +3,55 @@ name: release
on:
schedule:
- cron: '0 23 * * *'
-
workflow_dispatch:
jobs:
- release:
+ publish:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write
steps:
- name: Checkout
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_ADMIN_TOKEN }}
- - name: Setup Git
+ - name: Set up pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9.15.0
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: 'pnpm'
+ always-auth: true
+ registry-url: https://registry.npmjs.org
+
+ - name: Set up Git
run: |
- git config --local user.name "kettanaito"
+ git config --local user.name "Artem Zakharchenko"
git config --local user.email "kettanaito@gmail.com"
- name: Install dependencies
- run: yarn install --frozen-lockfile
+ run: pnpm install
+
+ - name: Install browsers
+ run: pnpm exec playwright install chromium --with-deps
+
+ - name: Build
+ run: pnpm build
+
+ - name: Tests (Node.js)
+ run: pnpm test:node
+
+ - name: Tests (browser)
+ run: pnpm test:browser
- - name: Release
- run: yarn release
+ - name: Publish
+ run: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GH_ADMIN_TOKEN }}
- NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 611ff96e..6ad7ad0a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,7 @@
+*.DS_Store
node_modules
+build
lib
-*.log
\ No newline at end of file
+dist
+test-results
+./tests/.tmp/**
\ No newline at end of file
diff --git a/.node-version b/.node-version
new file mode 100644
index 00000000..54c65116
--- /dev/null
+++ b/.node-version
@@ -0,0 +1 @@
+v24
diff --git a/.prettierrc b/.prettierrc.json
similarity index 50%
rename from .prettierrc
rename to .prettierrc.json
index ba44425d..65854ba4 100644
--- a/.prettierrc
+++ b/.prettierrc.json
@@ -1,6 +1,6 @@
{
"semi": false,
- "singleQuote": true,
"arrowParens": "always",
- "trailingComma": "all"
+ "trailingComma": "all",
+ "singleQuote": true
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 3662b370..00000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "typescript.tsdk": "node_modules/typescript/lib"
-}
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 00000000..f1314ab0
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2020–present Artem Zakharchenko
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/MIGRATING.md b/MIGRATING.md
new file mode 100644
index 00000000..d38da4cf
--- /dev/null
+++ b/MIGRATING.md
@@ -0,0 +1,148 @@
+# Migrating
+
+## v0 → v1
+
+Version 1.0 is a huge overhaul of the library to address some of the long-standing design issues and missing features. Please take your time to read through the [updated README](./README.md) to get familiar with the new concepts and APIs.
+
+When migrating from v0.x.x, please consult the guide below regarding the breaking changes to the library.
+
+### Terminology
+
+A few changes in the terminology to consider to make this migration guide easier to parse:
+
+- Model → Collection;
+- Entry → Record;
+- Relationship → Relation.
+
+### Package name
+
+The library has been migrated to a new package name—`@msw/data`.
+
+```diff
+-npm i @mswjs/data
++npm i @msw/data
+```
+
+### Deprecated: `factory`
+
+The model factory has been deprecated in favor of granular collections of data.
+
+```diff
+-const db = factory({ user: {} })
++const users = new Collection({ schema })
+```
+
+### Describing data
+
+The proprietary syntax of describing data has been deprecated in favor of the [Standard Schema](https://standardschema.dev/) for describing your data.
+
+```diff
+-factory({
+- firstName: () => 'John',
+- lastName: () => 'Maverick'
+-})
++new Collection({
++ schema: z.object({
++ firstName: z.string(),
++ lastName: z.string()
++ })
++})
+```
+
+> Although the example above is using [Zod](https://zod.dev/), you can use any Standard Schema-compatible schema library.
+
+### Deprecated: `primaryKey`
+
+The concept of a primary key has been deprecated entirely. You do not have to provide primary keys when describing your collections. You can query records by any properties.
+
+### Default values
+
+You can provide default values to the properties of your collection using the respective syntax of your schema library of choice. For example, here's how you list default values in Zod:
+
+```ts
+const users = new Collection({
+ schema: z.object({
+ subscribed: z.boolean().default(false)
+ })
+})
+
+await users.create() // { subscribed: false }
+```
+
+### Deprecated: `nullable`
+
+The `nullable` utility has been deprecated. Use your schema library of choice to describe nullable properties.
+
+### Deprecated: operators
+
+Operators like `equals`, `in`, `lgt`, etc. have been deprecated in favor of _queries_. You can express conditions for your records via literal values as well as function predicates.
+
+```ts
+// Find the first user with "id" equal to 1.
+users.findFirst(q => q.where({ id: 1 }))
+
+// Find all users whose last name is longer than 5 letters.
+users.findMany(
+ q => q.where({ lastName: lastName => lastName.length > 5 })
+)
+```
+
+> Learn more about [Querying](./README.md#querying).
+
+### Relations
+
+The `oneOf` and `manyOf` utilities has been deprecated. Use the `.defineRelations()` method on your collections to define relations between collections.
+
+```ts
+const userSchema = z.object({
+ id: z.number(),
+ get posts() {
+ return z.array(postSchema)
+ }
+})
+const postSchema = z.object({
+ title: z.string(),
+ get author() {
+ return userSchema
+ }
+})
+
+const users = new Collection({ schema: userSchema })
+const posts = new Collection({ schema: postSchema })
+
+users.defineRelations(({ many }) => ({
+ posts: many(posts)
+}))
+
+posts.defineRelations(({ one }) => ({
+ author: one(users)
+}))
+```
+
+> Learn more about [Relations](./README.md#relations).
+
+### Collocated updates
+
+You can now collocate updates of owner and foreign records in a relation by simply changing the foreign record's values as a part of the owner update.
+
+```ts
+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'
+ post.revision.updatedAt = Date.now()
+ }
+})
+```
+
+> The library will automatically translte the `updateAt` change of the reference revision into an implicit update.
+
+### Deprecated: `.toHandlers()`
+
+The `.toHandlers()` method has been deprecated. Generating request handlers is no longer a responsibility of this library. Instead, it should be possible via [Source](https://github.com/mswjs/source) (follow [this issue](https://github.com/mswjs/source/issues/80) for progress).
diff --git a/README.md b/README.md
index cd5e5f23..d48770b9 100644
--- a/README.md
+++ b/README.md
@@ -1,1196 +1,848 @@
+[standard-schema]: https://standardschema.dev/
+
-
+
+@msw/data
+Data querying library for testing JavaScript applications.
-@mswjs/data
+## Motivation
-Data modeling and relation library for testing JavaScript applications.
-
+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](https://mswjs.io) for seamless mocking experience both on the network and the data layers.
-## Motivation
+## Features
-When testing API interactions you often need to mock data. Instead of keeping a hard-coded set of fixtures, this library provides you with must-have tools for data-driven API mocking:
+- Relies on [Standard Schema][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](#querying) (inspired by Prisma);
+- Supports [relations](#relations) for database-like behaviors (inspired by Drizzle);
+- Supports extensions (including custom extensions) for things like cross-tab collection synchronization or record persistence.
-- An intuitive interface to model data;
-- The ability to create relationships between models;
-- The ability to query data in a manner similar to an actual database.
+---
## Getting started
### Install
-```bash
-$ npm install @mswjs/data --save-dev
-# or
-$ yarn add @mswjs/data --save-dev
```
-
-### Describe data
-
-With this library, you're modeling data using the `factory` function. That function accepts an object where each key represents a _model name_ and the values are _model definitions_. A model definition is an object where the keys represent model properties and the values are value getters.
-
-```js
-// src/mocks/db.js
-import { factory, primaryKey } from '@mswjs/data'
-
-export const db = factory({
- // Create a "user" model,
- user: {
- // ...with these properties and value getters.
- id: primaryKey(() => 'abc-123'),
- firstName: () => 'John',
- lastName: () => 'Maverick',
- },
-})
+npm i @msw/data --save-dev
```
-> See the [Recipes](#recipes) for more guidelines on data modeling.
+### Create collection
-Throughout this document native JavaScript constructors (i.e. String, Number) will be used as values getters for the models, as they both create a value and define its type. In practice, you may consider using value generators or tools like [Faker](#usage-with-faker) for value getters.
+You start by defining a data _collection_.
-#### Using the primary key
-
-Each model **must have a primary key**. That is a root-level property representing the model's identity. Think of it as an "id" column for a particular table in a database.
-
-Declare a primary key by using the `primaryKey` function:
-
-```js
-import { factory, primaryKey } from '@mswjs/data'
+```ts
+import { Collection } from '@msw/data'
+import { z } from 'zod'
-factory({
- user: {
- id: primaryKey(String),
- },
+const users = new Collection({
+ schema: z.object({
+ id: z.number(),
+ name: z.string(),
+ }),
})
```
-In the example above, the `id` is the primary key for the `user` model. This means that whenever a `user` is created it must have the `id` property that equals a unique `String`. Any property can be marked as a primary key, it doesn't have to be named "id".
+> Above, I'm using [Zod](https://zod.dev/) to describe the `schema` for my users collection. You can use whichever [Standard Schema][standard-schema] compliant library of your choice instead.
-Just like regular model properties, the primary key accepts a getter function that you can use to generate its value when creating entities:
+### Seed collection
-```js
-import { datatype } from '@faker-js/faker'
+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()`.
-factory({
- user: {
- id: primaryKey(datatype.uuid),
- },
-})
+```ts
+await users.create({ id: 1, name: 'John' })
+
+await users.createMany(5, (index) => ({
+ id: index + 1,
+ name: faker.person.firstName(),
+}))
```
-> Each time a new `user` is created, its `user.id` property is seeded with the value returned from the `datatype.uuid` function call.
+> Combine `.createMany()` with tools like [Faker](https://fakerjs.dev/) for random values in your records.
-Once your data is modeled, you can use [Model methods](#model-methods) to interact with it (create/update/delete). Apart from serving as interactive, queryable fixtures, you can also [integrate your data models into API mocks](#usage-with-api-mocks) to supercharge your prototyping/testing workflow.
+### Use collection
-## API
+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!
-- [`factory`](#factory)
-- [`primaryKey`](#primaryKey)
-- [`nullable`](#nullable)
-- [`oneOf`](#oneOf)
-- [`manyOf`](#manyOf)
-- [`drop`](#drop)
+---
-### `factory`
+## Querying
-The `factory` function is used to model a database. It accepts a _model dictionary_ and returns an API to interact with the described models.
+### Query syntax
-```js
-import { factory, primaryKey } from '@mswjs/data'
+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:
-const db = factory({
- user: {
- id: primaryKey(String),
- firstName: String
- age: Number
- }
-})
+```ts
+users.findFirst((q) => q.where({ name: 'John' }))
```
-> Learn more about the [Model methods](#model-methods) and how you can interact with the described models.
-
-Each `factory` call encapsulates an in-memory database instance that holds the respective models. It's possible to create multiple database instances by calling `factory` multiple times. The entities and relationships, however, are not shared between different database instances.
+> Above, we are defining a query using the `q` builder that targets the first user whose `name` property equals to `'John'`.
-### `primaryKey`
+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:
-Marks the property of a model as a primary key.
-
-```js
-import { factory, primaryKey } from '@mswjs/data'
-
-const db = factory({
- user: {
- id: primaryKey(String),
- },
-})
-
-// Create a new "user" with the primary key "id" equal to "user-1".
-db.user.create({ id: 'user-1' })
+```ts
+users.findFirst((q) =>
+ q.where({
+ name: (name) => name.startsWith('John'),
+ }),
+)
```
-Primary key must be unique for each entity and is used as the identifier to query a particular entity.
-
-### `nullable`
+> This query matches the first user whose `name` starts with `'John'`. Use functions as predicates to express more advanced logic in your queries.
-Marks the current model property as nullable.
+Query functions are supported at any level of nesting, including the top-level record itself.
-```js
-import { factory, primaryKey, nullable } from '@mswjs/data'
+```ts
+users.findFirst((q) => q.where((user) => user.posts.length > 0))
-factory({
- user: {
- id: primaryKey(String)
- // "user.title" is a nullable property.
- title: nullable(String)
- }
-})
+users.findFirst((q) =>
+ q.where({
+ address: {
+ street: (street) => street !== 'Baker st.',
+ },
+ }),
+)
```
-> Learn more how to work with [Nullable properties](#nullable-properties).
+#### Logical operators
-### `oneOf`
+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:
-Creates a `*-to-one` relationship with another model.
+```ts
+users.findMany((q) =>
+ q.where({ posts: (posts) => posts.length > 0 }).or({ role: 'editor' }),
+)
+```
-```js
-import { factory, primaryKey, oneOf } from '@mswjs/data'
+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.
-factory({
- user: {
- id: primaryKey(String),
- role: oneOf('userGroup'),
- },
- userGroup: {
- name: primaryKey(String),
- },
-})
+```ts
+users.findMany((q) =>
+ q.or(
+ q.where({ posts: (posts) => posts.length > 0 }),
+ q.where({ role: 'editor' }),
+ ),
+)
```
-> Learn more about [Modeling relationships](#model-relationships).
+## Pagination
-### `manyOf`
+This library supports offset and cursor-based pagination.
-Creates a `*-to-many` relationship with another model.
+### Offset-based pagination
-```js
-import { factory, primaryKey, manyOf } from '@mswjs/data'
+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.
-factory({
- user: {
- id: primaryKey(String),
- publications: manyOf('post'),
- },
- post: {
- id: primaryKey(String),
- title: String,
+```ts
+const users = new Collection({ schema })
+
+users.findMany(
+ (q) => q.where({ email: (email) => email.includes('@google.com') }),
+ {
+ // Return the first 5 matching records.
+ take: 5,
},
-})
+)
```
-> Learn more about [Modeling relationships](#model-relationships).
-
-### `drop`
-
-Deletes all entities in the given database instance.
+You can also skip the number of first matching results by providing the `skip` property:
-```js
-import { factory, drop } from '@mswjs/data'
-
-const db = factory(...models)
+```ts
+const users = new Collection({ schema })
-drop(db)
+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,
+ },
+)
```
-## Model methods
-
-Each model has the following methods:
+The negative value for `take` is also supported to have backward pagination:
-- [`create()`](#create)
-- [`findFirst()`](#findFirst)
-- [`findMany()`](#findMany)
-- [`count()`](#count)
-- [`getAll()`](#getAll)
-- [`update()`](#update)
-- [`updateMany()`](#updateMany)
-- [`delete()`](#delete)
-- [`deleteMany()`](#deleteMany)
-- [`toHandlers()`](#toHandlers)
+```ts
+users.findMany(
+ (q) => q.where({ email: (email) => email.includes('@google.com') }),
+ {
+ take: -5,
+ },
+)
+````
-### `create`
+### Cursor-based pagination
-Creates an entity for the model.
+Provide a reference to the record of the same collection as the `cursor` property for cursor-based pagination.
-```js
-const user = db.user.create()
-```
-
-When called without arguments, `.create()` will populate the entity properties using the getter functions you've specified in the model definition.
+```ts
+const users = new Collection({ schema })
-You can also provide a partial initial values when creating an entity:
+const john = users.findFirst((q) => q.where({ name: 'John' }))
-```js
-const user = db.user.create({
- firstName: 'John',
+users.findMany((q) => q.where({ subscribed: true }), {
+ cursor: john,
+ take: 5,
})
```
-> Note that all model properties _are optional_, including [relational properties](#model-relationships).
+## Sorting
-### `findFirst`
+You can sort the results of bulk operations, like `.findMany()`, `.updateMany()`, and `.deleteMany()`, by providing the `orderBy` property in that operation's options.
-Returns the first entity that satisfies the given query.
-
-```js
-const user = db.user.findFirst({
- where: {
- id: {
- equals: 'abc-123',
- },
- },
+```ts
+// 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' },
})
```
-### `findMany`
+You can sort by multiple keys by listing them in the `orderBy` object:
-Returns all the entities that satisfy the given query.
-
-```js
-const users = db.user.findMany({
- where: {
- followersCount: {
- gte: 1000,
- },
+```ts
+users.updateMany((q) => q.where({ name: (name) => name.startsWith('J') }), {
+ data(user) {
+ user.name = user.name.toUpperCase()
+ },
+ orderBy: {
+ name: 'asc',
+ id: 'desc',
},
})
```
-### `count`
-
-Returns the number of records for the given model.
+You can sort by an ordered list of criteria by passing an array to `orderBy`. Each entry is applied in sequence: the first entry determines the primary sort, and each subsequent entry breaks ties among records that compare equal under the preceding criteria.
-```js
-db.user.create()
-db.user.create()
-
-db.user.count() // 2
+```ts
+users.findMany(undefined, {
+ orderBy: [{ age: 'asc' }, { name: 'desc' }]
+})
```
-Can accept an optional query argument to filter the records before counting them.
+## Relations
-```js
-db.user.count({
- where: {
- role: {
- equals: 'reader',
- },
- },
-})
-```
+You can define relations by calling the `.defineRelations()` method on the collection.
-### `getAll`
+- [One-to-one](#one-to-one)
+- [One-to-many](#one-to-many)
+- [One-to-many (inversed)](#one-to-many-inversed)
+- [Many-to-many](#many-to-many)
+- [Through relations](#through-relations)
+- [Unique relations](#unique-relations)
+- [Ambiguous relations](#ambiguous-relations)
+- [Polymorphic relations](#polymorphic-relations)
-Returns all the entities of the given model.
+### Defining relations
-```js
-const allUsers = db.user.getAll()
-```
+Below, you can find examples of defining various types of relations, but there are a few things that apply to all of them:
-### `update`
+- 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 `foreignKey` associations and instead are bound to internal IDs of related records.
-Updates the first entity that matches the query.
+### One-to-one
-```js
-const updatedUser = db.user.update({
- // Query for the entity to modify.
- where: {
- id: {
- equals: 'abc-123',
- },
- },
- // Provide partial next data to be
- // merged with the existing properties.
- data: {
- // Specify the exact next value.
- firstName: 'John',
-
- // Alternatively, derive the next value from
- // the previous one and the unmodified entity.
- role: (prevRole, user) => reformatRole(prevRole),
+```ts
+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() })
-### `updateMany`
+const users = new Collection({ schema: userSchema })
+const countries = new Collection({ schema: countrySchema })
-Updates multiple entities that match the query.
+// Declare the relations on the `users` collection.
+users.defineRelations(({ one }) => ({
+ // `user.country` is a one-of relation to the `countries` collection.
+ country: one(countries),
+}))
-```js
-const updatedUsers = db.user.updateMany({
- // Query for the entity to modify.
- where: {
- id: {
- in: ['abc-123', 'def-456'],
- },
- },
- // Provide partial next data to be
- // merged with the existing properties.
- data: {
- firstName: (firstName) => firstName.toUpperCase(),
- },
+const user = await users.create({
+ country: await countries.create({ code: 'usa' }),
})
+user.country // { code: 'usa' }
```
-### `delete`
-
-Deletes the entity that satisfies the given query.
+### One-to-many
-```js
-const deletedUser = db.user.delete({
- where: {
- followersCount: {
- equals: 0,
- },
+```ts
+const postSchema = z.object({
+ get comments() {
+ return z.array(countrySchema)
},
})
-```
+const commentSchema = z.object({
+ text: z.string(),
+})
-### `deleteMany`
+const posts = new Collection({ schema: postSchema })
+const comments = new Collection({ schema: commentSchema })
-Deletes multiple entities that match the query.
+posts.defineRelations(({ many }) => ({
+ comments: many(comments),
+}))
-```js
-const deletedUsers = db.user.deleteMany({
- where: {
- followersCount: {
- lt: 10,
- },
- },
+await posts.create({
+ comments: [
+ await comments.create({ text: 'First!' }),
+ await comments.create({ text: 'Thanks for watching.' }),
+ ],
})
```
-### `toHandlers`
-
-Generates request handlers for the given model to use with [Mock Service Worker](https://github.com/mswjs/msw). All generated handlers are automatically connected to the respective [model methods](#model-methods), enabling you to perform CRUD operations against your mocked database.
-
-#### REST handlers
+### One-to-many (inversed)
-```js
-import { factory, primaryKey } from '@mswjs/data'
+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 db = factory({
- user: {
- id: primaryKey(String),
- firstName: String,
+```ts
+const postSchema = z.object({
+ get comments() {
+ return z.array(countrySchema)
},
})
-
-// Generates REST API request handlers.
-db.user.toHandlers('rest')
-```
-
-- Learn more about [REST API mocking integration](#generate-rest-api).
-
-#### GraphQL handlers
-
-```js
-import { factory, primaryKey } from '@mswjs/data'
-
-const db = factory({
- user: {
- id: primaryKey(String),
- firstName: String,
+const commentSchema = z.object({
+ text: z.string(),
+ get post() {
+ return postSchema
},
})
-// Generates GraphQL API request handlers.
-db.user.toHandlers('graphql')
-```
-
-- Learn more about [GraphQL API mocking integration](#generate-graphql-api).
+const posts = new Collection({ schema: postSchema })
+const comments = new Collection({ schema: commentSchema })
-#### Scoping handlers
+posts.defineRelations(({ many }) => ({
+ comments: many(comments),
+}))
+comments.defineRelations(({ one }) => ({
+ post: one(posts),
+}))
-The `.toHandlers()` method supports an optional second `baseUrl` argument to scope the generated handlers to a given endpoint:
+await posts.create({
+ comments: [await comments.create({ text: 'First!' })],
+})
-```js
-db.user.toHandlers('rest', 'https://example.com')
-db.user.toHandlers('graphql', 'https://example.com/graphql')
+const comment = comments.findFirst((q) => q.where({ text: 'First!' }))
+comment.post // { comments: [{ text: 'First', post: Circular }] }
```
-## Recipes
-
-- **Modeling:**
- - [Nullable properties](#nullable-properties)
- - [Nested structures](#nested-structures)
- - [Model relationships](#model-relationships)
-- **Querying:**
- - [Querying data](#querying-data)
- - [Strict mode](#strict-mode)
- - [Pagination](#pagination)
- - [Sorting](#sorting)
-
-### Nullable properties
+> Inversed relations are updated automatically. Whenever you add a new comment to a post, both `post.comments` and `comment.post` are updated to reference each other. The same is true when setting a new parent `post` on the comment.
-By default, all model properties are non-nullable. You can use the `nullable` function to mark a property as nullable:
+### Many-to-many
-```js
-import { factory, primaryKey, nullable } from '@mswjs/data'
+In the next example, every `user` may have multiple `posts` while each `post` may have multiple `authors`.
-const db = factory({
- user: {
- id: primaryKey(String),
- firstName: String,
- // "user.age" is a nullable property.
- age: nullable(Number),
+```ts
+const userSchema = z.object({
+ get posts() {
+ return z.array(postSchemas)
},
})
-
-db.user.create({
- id: 'user-1',
- firstName: 'John',
- // Nullable properties can be explicit null as the initial value.
- age: null,
-})
-
-db.user.update({
- where: {
- id: {
- equals: 'user-1',
- },
- },
- data: {
- // Nullable properties can be updated to null.
- age: null,
+const postSchema = z.object({
+ get authors() {
+ return z.array(userSchema)
},
})
-```
-
-> You can define [Nullable relationships](#nullable-relationships) in the same manner.
-
-When using Typescript, you can manually set the type of the property when it cannot be otherwise inferred from the seeding function, such as when you want a property to default to `null`:
-```typescript
-import { factory, primaryKey, nullable } from '@mswjs/data'
+const users = new Collection({ schema: userSchema })
+const posts = new Collection({ schema: postSchema })
-const db = factory({
- user: {
- id: primaryKey(String),
- age: nullable(() => null),
- },
-})
+users.defineRelations(({ many }) => ({
+ posts: many(posts),
+}))
+posts.defineRelations(({ many }) => ({
+ authors: many(users),
+}))
```
-### Nested structures
+### Through relations
-You may use nested objects to design a complex structure of your model:
+Since relational properties resolve via getters, there's no need to define special "through" relations to reference one model through a relation from another.
-```js
-import { factory, primaryKey, nullable } from '@mswjs/data'
-
-const db = factory({
- user: {
- id: primaryKey(String),
- address: {
- billing: {
- street: String,
- city: nullable(String),
- },
- },
- },
-})
+```ts
+const owners = new Collection({ schema: ownerSchema })
+const cars = new Collection({ schema: carSchema })
+const mechanics = new Collection({ schema: mechanicSchema })
-// You can then create and query your data
-// based on the nested properties.
+owners.defineRelations(({ many }) => ({
+ cars: many(cars),
+}))
+cars.defineRelations(({ one }) => ({
+ owner: one(owners),
+}))
+mechanics.defineRelations(({ one }) => ({
+ car: one(cars),
+}))
-db.user.create({
- id: 'user-1',
- address: {
- billing: {
- street: 'Baker st.',
- city: 'London',
- },
- },
-})
+const owner = await owners.create({ name: 'John' })
+const car = await cars.create({ brand: 'bmw', owner })
+const mechanic = await mechanics.create({ name: 'Kyle', car })
-db.user.update({
- where: {
- id: {
- equals: 'user-1',
- },
- },
- data: {
- address: {
- billing: {
- street: 'Sunwell ave.',
- city: null,
- },
- },
- },
-})
+mechanic.car.owner.name // "John"
```
-> Note that you **cannot** mark a nested property as the [primary key](#using-the-primary-key).
+> Note that although `mechanics` does not define an explicit relation to `owners`, you can get the owner of the car associated with a mechanic through the `car` relation.
-You may also specify _relationships_ nested deeply in your model:
+### Unique relations
-```js
-factory({
- user: {
- id: primaryKey(String),
- address: {
- billing: {
- country: oneOf('country'),
- },
- },
- },
- country: {
- code: primaryKey(String),
- },
-})
-```
+You can mark a relation as unique by setting the `unique` property of the relation options to `true`. Unique relations cannot reference foreign records that are already associated with other owner records.
-> Learn more about [Model relationships](#model-relationships).
-
-### Model relationships
+```ts
+posts.defineRelations(({ one }) => ({
+ author: one(users, { unique: true }),
+}))
+```
-- [One-to-One](#one-to-one)
-- [One-to-Many](#one-to-many)
-- [Many-to-One](#many-to-one)
-- [Unique relationships](#unique-relationships)
-- [Nullable relationships](#nullable-relationships)
+> In this example, the `author` of each post points to a single _unique_ user. If a post attempts to set its author to a user that's already associated with another post, an error will be thrown.
-Relationship is a way for a model to reference another model.
+### Ambiguous relations
-#### One-to-One
+You can use the `role` option of the relation to disambiguate between multiple properties referencing the same foreign model.
-```js
-import { factory, primaryKey, oneOf } from '@mswjs/data'
+For example, a single `post` may have both `author` and `reviewer` referencing the same `user` model. To make those properties pointing to _different_ user records, use the `role` that acts as a relation identifier. This way, the library will update the corresponding relational properties for both `users` and `posts` when the referenced relation is updated.
-const db = factory({
- user: {
- id: primaryKey(String),
- firstName: String,
- },
- post: {
- id: primaryKey(String),
- title: String,
- // The "post.author" references a "user" model.
- author: oneOf('user'),
- },
-})
+```ts
+const users = new Collection({ schema: userSchema })
+const posts = new Collection({ schema: postSchema })
-const user = db.user.create({ firstName: 'John' })
-const post = db.post.create({
- title: 'My journey',
- // Use a "user" entity as the actual value of this post's author.
- author: user,
-})
+users.defineRelations(({ many }) => ({
+ posts: many(posts, { role: 'author' }),
+ reviews: many(posts, { role: 'reviewer' }),
+}))
-post.author.firstName // "John"
+posts.defineRelations(({ one }) => ({
+ author: one(user, { role: 'author' }),
+ reviewer: one(user, { role: 'reviewer' }),
+}))
```
-#### One-to-Many
-
-```js
-import { factory, primaryKey, manyOf } from '@mswjs/data'
+> The `role` property acts as a de-facto ID of a relation when synchronizing related models.
-const db = factory({
- user: {
- id: primaryKey(String),
- // "user.posts" is a list of the "post" entities.
- posts: manyOf('post'),
- },
- post: {
- id: primaryKey(String),
- title: String,
- },
-})
+### Polymorphic relations
-const posts = [
- db.post.create({ title: 'First' }),
- db.post.create({ title: 'Second' }),
-]
+Provide an array of foreign collections to a relation to define it as _polymorphic_.
-const user = db.user.create({
- // Assign the list of existing posts to this user.
- posts,
-})
+```ts
+const posts = new Collection({ schema: postSchema })
+const images = new Collection({ schema: imageSchema })
+const videos = new Collection({ schema: videoSchema })
-user.posts // [{ title: "First" }, { title: "Second" }]
+posts.defineRelations(({ many }) => ({
+ // Providing a record of foreign collections allows
+ // all of their records to be set as the value.
+ attachments: many([images, videos]),
+}))
+images.defineRelations(({ one }) => ({
+ post: one(posts),
+}))
+videos.defineRelations(({ one }) => ({
+ post: one(posts),
+}))
```
-#### Many-to-One
+> In this example, `post.attachments` is an array of either `images` or `videos`, where records from both collections are allowed.
-```js
-import { factory, primaryKey, oneOf } from '@mswjs/data'
+## Error handling
-const db = factory({
- country: {
- name: primaryKey(String),
- },
- user: {
- id: primaryKey(String),
- country: oneOf('country'),
- },
- car: {
- serialNumber: primaryKey(String),
- country: oneOf('country'),
- },
-})
+Data provides multiple different error classes to help you differentiate and handle different errors.
-const usa = db.country.create({ name: 'The United States of America' })
+### `OperationError`
-// Create a "user" and a "car" with the same country.
-db.user.create({ country: usa })
-db.car.create({ country: usa })
-```
+- `code` ``, the error code describing the failed operation;
+- `cause` `` (_optional_), a reference to the original thrown error.
-#### Unique relationships
+Thrown whenever performing a record operation fails. For example:
-Both `oneOf` and `manyOf` relationships may be marked as unique. A unique relationship is where a referenced entity cannot be assigned to another entity more than once.
+- When creating a new record whose initial values do not match the collection's schema;
+- When there are no records found for a strict query.
-In the example below we define the "user" and "invitation" models, and design their relationship so that one invitation cannot be assigned to multiple users.
+### `RelationError`
-```js
-import { factory, primaryKey, oneOf } from '@mswjs/data'
+- `code` ``, the error code describing the relation operation;
+- `info` ``, additional error information;
+ - `path` ``, path of the relational property;
+ - `ownerCollection` ``, a reference to the owner collection;
+ - `foreignCollection` `>`, an array of foreign collections referenced by this relation;
+ - `options` `RelationDefinitionOptions`, the options object passed upon decaring this relation.
-const db = factory({
- user: {
- id: primaryKey(String),
- invitation: oneOf('invitation', { unique: true }),
- },
- invitation: {
- id: primaryKey(String),
- },
-})
+Thrown whenever performing a relation operation fails. For example:
-const invitation = db.invitation.create()
+- When attempting to reference a foreign record that's already associated with another record in a unique relation;
+- When directly assigning value to a relational property.
-const john = db.user.create({ invitation })
+---
-// Assigning the invitation already used by "john"
-// will throw an exception when creating this entity.
-const karl = db.user.create({ invitation })
-```
+## API
-#### Nullable relationships
+### `new Collection(options)`
-Both `oneOf` and `manyOf` relationships may be passed to `nullable` to allow
-instantiating and updating that relation to null.
+- `options` ``
+ - `schema` [Standard Schema][standard-schema] A schema describing each record in this collection.
+ - `extensions` (optional) An array of [extensions](#extensions) to use on this collection.
-```js
-import { factory, primaryKey, oneOf, nullable } from '@mswjs/data'
+Creates a new collection of data.
-const db = factory({
- user: {
- id: primaryKey(String),
- invitation: nullable(oneOf('invitation')),
- friends: nullable(manyOf('user')),
- },
- invitation: {
- id: primaryKey(String),
- },
-})
+#### `.create(initialValues)`
-const invitation = db.invitation.create()
+- `initialValues` Initial values for the new record.
-// Nullable relationships are instantiated with null.
-const john = db.user.create({ invitation }) // john.friends === null
-const kate = db.user.create({ friends: [john] }) // kate.invitation === null
+Creates a single record with the provided initial values.
-db.user.updateMany({
- where: {
- id: {
- in: [john.id, kate.id],
- },
- },
- data: {
- // Nullable relationships can be updated to null.
- invitation: null,
- friends: null,
- },
-})
+```ts
+const user = await users.create({ id: 1, name: 'John' })
```
-### Querying data
-
-This library supports querying of the seeded data similar to how one would query a SQL database. The data is queried based on its properties. A query you construct depends on the value type you are querying.
+> The `.create()` method returns a promise to support potential asynchronous transformations in your schema.
-#### String operators
+#### `.createMany(count, initialValuesFactory)`
-- `equals`
-- `notEquals`
-- `contains`
-- `notContains`
-- `in`
-- `notIn`
+- `count` `` A number of records to create.
+- `initialValuesFactory` `` A function that returns initial values for each record.
-#### Number operators
+Creates multiple records with the initial value factory.
-- `equals`
-- `notEquals`
-- `gt`
-- `gte`
-- `lt`
-- `lte`
-- `between`
-- `notBetween`
-- `in`
-- `notIn`
-
-#### Boolean operators
+```ts
+const users = await users.createMany(5, (index) => ({
+ id: index + 1,
+ name: 'John',
+}))
+```
-- `equals`
-- `notEquals`
+The initial value factory function accepts the `index` argument indicating the index of the record that's being created. Use it, as well as the function's closure, to generate unique or random values.
-#### Date operators
+#### `.findFirst(query)`
-- `equals`
-- `notEquals`
-- `gt`
-- `gte`
-- `lt`
-- `lte`
+- `query` [`Query`](#new-querypredicate) A query matching the record.
-#### Query example
+Returns the first record matching the query.
-```js
-const db = factory({
- post: {
- id: String,
- likes: Number,
- isDraft: Boolean,
- },
+```ts
+const users = new Collection({
+ schema: z.object({
+ id: z.number(),
+ name: z.string(),
+ }),
})
-// Returns the list of `post` entities
-// that satisfy the given query.
-const popularPosts = db.post.findMany({
- where: {
- likes: {
- gte: 1000,
- },
- isDraft: {
- equals: false,
- },
- },
-})
+await users.create({ id: 1, name: 'John' })
+await users.create({ id: 2, name: 'John' })
+
+users.findFirst((q) => q.where({ name: 'John' }))
+// { id: 1, name: 'John' }
```
-### Strict mode
+#### `.findMany(query)`
-When querying or updating the entities you can supply the `strict: boolean` property on the query. When supplied, if a query operation fails (i.e. no entity found), the library will throw an exception.
+- `query` [`Query`](#new-querypredicate) A query matching the records.
-```js
-import { factory, primaryKey } from '@mswjs/data'
+Returns all records matching the query.
-const db = factory({
- user: {
- id: primaryKey(String),
- },
+```ts
+const users = new Collection({
+ schema: z.object({
+ id: z.number(),
+ name: z.string(),
+ }),
})
-db.user.create({ id: 'abc-123' })
+await users.create({ id: 1, name: 'John' })
+await users.create({ id: 2, name: 'John' })
-// This will throw an exception, because there are
-// no "user" entities matching this query.
-db.user.findFirst({
- where: {
- id: {
- equals: 'def-456',
- },
- },
- strict: true,
-})
+users.findMany((q) => q.where({ name: 'John' }))
+// [{ id: 1, name: 'John' }, { id: 2, name: 'John' }]
```
-### Pagination
+#### `.update(query, options)`
-This library supports _offset-based_ and _cursor-based_ pagination of the `findMany` method results.
+- `query` [`Query`](#new-querypredicate) A query matching the record.
+- `options` ``
+ - `data` A function that produces changes by modifying the previous record.
-#### Offset-based pagination
+Updates the first record matching the query. Returns a promise that resolves with the updated record.
-```js
-const db = factory({
- post: {
- id: primaryKey(String),
- category: String,
- },
-})
-
-db.post.findMany({
- where: {
- category: {
- equals: 'Science',
- },
+```ts
+// Change the name for the user with a specific `id`.
+const updatedUser = await users.update((q) => q.where({ id: 123 }), {
+ data(user) {
+ user.name = 'Johnatan'
},
- take: 15,
- skip: 10,
})
```
-#### Cursor-based pagination
+> Update methods return a promise in order to support potential asynchronous transformations defined in your schema.
-The `cursor` option of the `findMany` query expects a primary key value of a model to start the pagination from.
+The `data` function allows you to perform multiple updates upon a record by mutating that record directly. Think of it as a draft function from libraries like `immer` or `mutative` because that's precisely what it is!
-```js
-const db = factory({
- post: {
- // The `id` primary key will be used as a cursor.
- id: primaryKey(String),
- category: String,
- },
-})
+You can also provide a record reference as the predicate to the `.update()` method to update that particular record:
-const firstPage = db.post.findMany({
- where: {
- category: {
- equals: 'Science',
- },
- },
- take: 15,
- cursor: null,
-})
-
-const secondPage = db.post.findMany({
- where: {
- category: {
- equals: 'Science',
- },
+```ts
+const user = users.findFirst((q) => q.where({ id: 123 }))
+await users.update(user, {
+ // 👆👆
+ data(user) {
+ user.id = 456
},
- take: 15,
- // The second page will start from the last post
- // of the `firstPage`.
- cursor: firstPage[firstPage.length - 1].id,
})
```
-### Sorting
+#### `.updateMany(query, options)`
-#### Basic sorting
+- `query` [`Query`](#new-querypredicate) A query matching the records.
+- `options` ``
+ - `data` Changes to apply to each record.
-```js
-const db = factory({
- post: {
- id: primaryKey(String),
- title: String,
- },
-})
+Updates all records matching the query. Returns a promise that resolves with an array containing the updated records.
-// Return first 10 posts in the "Science" category
-// sorted by the post's "title".
-db.post.findMany({
- where: {
- category: {
- equals: 'Science',
- },
- },
- take: 10,
- orderBy: {
- title: 'asc',
+```ts
+// Find all the users with the name "John"
+// and make their name truly stand out!
+const updatedUsers = await users.updateMany((q) => q.where({ name: 'John' }), {
+ data(user) {
+ user.name = user.name.toUpperCase()
},
})
```
-> You can use `orderBy` with [pagination](#pagination).
+#### `.delete(query)`
-#### Sorting by relational properties
+- `query` [`Query`](#new-querypredicate) A query matching the record.
-```js
-const db = factory({
- post: {
- id: primaryKey(String),
- title: String,
- author: oneOf('user'),
- },
- user: {
- id: primaryKey(String),
- firstName: String,
- },
-})
+Deletes the first record matching the query. Returns the deleted record.
-// Return all posts in the "Science" category
-// sorted by the post author's first name.
-db.post.findMany({
- where: {
- category: {
- equals: 'Science',
- },
- },
- orderBy: {
- author: {
- firstName: 'asc',
- },
- },
-})
+```ts
+// Delete a user with a particular `id`.
+const deletedUser = users.delete((q) => q.where({ id: 123 }))
```
-#### Sorting by multiple criteria
-
-Provide a list of criteria to sort the query result against.
+You can also provide a record reference as the predicate to the `.delete()` method to delete that particular record:
-```js
-db.post.findMany({
- orderBy: [
- {
- title: 'asc',
- },
- {
- views: 'desc',
- },
- ],
-})
+```ts
+const user = users.findFirst((q) => q.where({ id: 123 }))
+users.delete(user)
```
-You can also use a combination of direct and relational properties on a single query:
+#### `.deleteMany(query)`
-```js
-db.post.findMany({
- orderBy: [
- {
- title: 'asc',
- },
- {
- author: {
- firstName: 'asc',
- },
- },
- ],
-})
-```
+- `query` [`Query`](#new-querypredicate) A query matching the records.
-### Database utilities
+Deletes all records matching the query. Returns an array containing the deleted records.
-#### `drop`
+```ts
+// Delete all users whose trial period has expired.
+const deletedUsers = users.deleteMany((q) =>
+ q.where({ trial: { expiresAt: (expiresAt) => expiresAt <= Date.now() } }),
+)
+```
-Drops the given database, deleting all its entities.
+#### `.defineRelations(definition)`
-```js
-import { factory, drop } from '@mswjs/data'
+- `definition` `` A function that accepts relation utilities and returns an object with relational properties.
-const db = factory({...})
+Defines relations on the current collection.
-drop(db)
-```
+```ts
+const users = new Collection({ schema: userSchema })
+const posts = new Collection({ schema: postSchema })
-### Usage with `Faker`
+users.defineRelations(({ many }) => ({
+ // `user.posts` is a many-of relation to `posts`.
+ posts: many(posts)
+}))
+```
-Libraries like [Faker](https://github.com/faker-js/faker) can help you generate fake data for your models.
+> You can define nested relational properties by nesting them in the object returned from `.defineRelations()`.
-```js
-import { seed, datatype, name } from '@faker-js/faker'
-import { factory, primaryKey } from '@mswjs/data'
+##### Relational utilities
-// (Optional) Seed `faker` to ensure reproducible
-// random values of model properties.
-seed(123)
+The following relational utilities are exposed in the argument to this method:
-factory({
- user: {
- id: primaryKey(datatype.uuid),
- firstName: name.firstName,
- },
-})
-```
+- `one(collection[, options])`, defines a one-of relation to the given collection;
+- `many(collection[, options])`, defines a many-of relation to the given collection.
-### Collocated updates
+##### Relation options
-When you wish to update a parent entity and one of its relational properties at the same time, collocate such an update operation via the updater function of the [`update`](#update) method.
+- `unique` ``, marks this relation as unique. Foreign records referenced by this relation cannot be referenced by other models.
-```js
-import { factory, primaryKey, oneOf } from '@mswjs/data'
+```ts
+users.defineRelations(({ many }) => ({
+ posts: many(posts)
+}))
+posts.defineRelations(({ one }) => ({
+ author: one(users, { unique: true })
+}))
-const db = factory({
- post: {
- id: primaryKey(String),
- title: String,
- revision: oneOf('revision'),
- },
- revision: {
- id: primaryKey(String),
- updatedAt: () => new Date(),
- },
+const john = await users.create({
+ name: 'John',
+ // `john` is associated as the `author` of this post now.
+ posts: [await posts.create({ title: 'First post' })]
})
-db.post.update({
- where: {
- id: { equals: 'post-1' },
- },
- data: {
- title: 'Renamed post',
- // The next value of the "post.revision"
- // is returned from this updater function.
- revision(prevRevision, post) {
- // Update this post's revision as you'd do usually,
- // but nested within the post's update operation.
- return db.revision.update({
- where: {
- id: { equals: post.revision.id },
- },
- data: {
- updatedAt: Date.now(),
- },
- })
- },
- },
+await users.create({
+ name: 'Katy',
+ // Creating this user will error because it tries to list
+ // a post which `author` already references to another user.
+ posts: [john.posts[0]]
})
```
-> While the `post` above will get updated, both `post.revision` and the respective `revision` standalone will be updated as well.
+- `role` ``, an identifier to differentiate ambiguous relations to the same foreign collection;
-Collocating nested updates grants you a predictable behavior when changing multiple related entities.
+```ts
+users.defineRelations(({ many }) => ({
+ // Both `users` and `posts` reference each other in multiple keys.
+ // Using `role` helps the library understand which keys are connected.
+ posts: many(posts, { role: 'author' }),
+ underReview: many(posts, { role: 'reviewer' })
+}))
-## Usage with API mocks
+posts.defineRelations(({ one, many }) => ({
+ author: one(users, { role: 'author' }),
+ reviewers: many(users, { role: 'reviewer' })
+}))
+```
-While this library can be used standalone, it brings a tremendous benefit in a combination with tools like [Mock Service Worker](https://github.com/mswjs). We provide a build-in API to quickly generate API request handlers based on your models, representing model interactions via HTTP requests.
+- `onDelete` `"cascade" | undefined`, decides how to handle referenced foreign records when the owner is deleted.
-### Generate request handlers
+```ts
+users.defineRelations(({ many }) => ({
+ // If a user gets deleted, delete all of the `posts` associated with them.
+ posts: many(posts, { onDelete: 'cascade' })
+}))
+posts.defineRelations(({ one }) => ({
+ author: one(users)
+}))
+```
-Both REST and GraphQL [request handlers]() can be generated from a model using the [`.toHandlers()`](#toHandlers) method of that model. When generated, request handlers automatically have that model's CRUD methods like `POST /user` or `mutation CreateUser`.
+### `new Query([predicate])`
-#### Generate REST API
+- `predicate` (optional) An object or a function that acts as a predicate for records.
-REST API request handlers can be generated by calling the `.toHandlers('rest')` method on the respective factory model.
+Creates a new query to match records in a collection. Normally, you query records through the querying methods of the collection (see [Querying](#querying)). You can, however, construct a type-safe `Query` class to abstract common queries or query builders.
```ts
-import { setupServer } from 'msw/node'
-import { factory, primaryKey } from '@mswjs/data'
-
-const db = factory({
- user: {
- id: primaryKey(String),
- firstName: String,
- },
+const userSchema = z.object({
+ id: z.number(),
+ subscribed: z.boolean().default(false),
+ role: z.enum(['user', 'editor', 'admin']).default('user'),
})
-const handlers = [...db.user.toHandlers('rest')]
-
-// Establish requests interception.
-const server = setupServer(...handlers)
-server.listen()
+// Creates a query builder for the users schema.
+const query = new Query()
```
-Given the "user" model definition above, the following request handlers are generated and connected to the respective database operations:
+#### `.where(predicate)`
-- `GET /users/:id` (where "id" is your model's primary key), returns a user by ID;
-- `GET /users`, returns all users (supports [pagination](#pagination));
-- `POST /users`, creates a new user;
-- `PUT /users/:id`, updates an existing user by ID;
-- `DELETE /users/:id`, deletes an existing user by ID;
+- `predicate` A predicate for the records.
+- Returns: [`Query`](#new-querypredicate).
-The "/user" part of the route is derived from your model name. For example, if you had a "post" model defined in your `factory`, then the generated handlers would be `/posts`, `/posts/:id`, etc.
+```ts
+query.where({ id: 2 })
+```
-With the request handlers generated and MSW configured, you can query the "database" using REST API:
+#### `.or(predicate)`
-```js
-// Create a new user in the database.
-fetch('/users', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- id: 'abc-123',
- firstName: 'John',
- }),
-})
+- `predicate` A predicate or another [`Query`](#new-querypredicate).
+- Returns: [`Query`](#new-querypredicate).
+
+Creates a new query, merging the previous predicates with the new one under a `OR` relation. A record may match _any predicate_ to be considered matching.
+
+```ts
+const unsubscribedOrEditorsQuery = query.or(
+ query.where({ subscribed: false }),
+ query.where({ role: 'editor' }),
+)
```
-#### Generate GraphQL API
+#### `.and(predicate)`
-GraphQL API request handlers can be generated by calling the `.toHandlers('graphql')` method on the respective factory model.
+- `predicate` A predicate or another [`Query`](#new-querypredicate).
+- Returns: [`Query`](#new-querypredicate).
-```js
-import { setupServer } from 'msw/node'
-import { factory, primaryKey } from '@mswjs/data'
+Creates a new query, merging the previous predicates with the new one under a `AND` relation. A record must match _all predicates_ to be considered matching.
-const db = factory({
- user: {
- id: primaryKey(String),
- firstName: String,
- },
-})
+```ts
+const exactAdminQuery = query.and(
+ query.where({ id: 1 }),
+ query.where({ role: 'admin' }),
+)
+```
-const handlers = [...db.user.toHandlers('graphql')]
+#### `.test(record)`
-// Establish requests interception.
-const server = setupServer(...handlers)
-server.listen()
+- `record` `` A reference to a record to test.
+- Returns `` Indicates whether the given record matches the query.
+
+```ts
+query.test({ id: 1 })
```
-Given the "user" model definition above, the following request handlers are generated and connected to the respective database operations:
+---
-- `user(where: UserQueryInput): User`, returns a user matching the query;
-- `users(where: UserQueryInput, cursor: ID, skip: Int, take: Int): [User!]`, returns all users matching the query (supports [pagination](#pagination));
-- `createUser(data: UserInput!): User!`, creates a new user;
-- `updateUser(where: UserQueryInput!, data: UserInput!): User!`, updates a user that match the `where` query;
-- `updateUsers(where: UserQueryInput!, data: UserInput!): [User!]`, updates multiple users that match the `where` query;
-- `deleteUser(where: UserQueryInput!): User!`, deletes a user that match the `where` query;
-- `deleteUsers(where: UserQueryInput!): [User!]`, deletes multiple users that match the `where` query.
+## Extensions
-The "User" part of the GraphQL operation names is derived from your model's name. For example, if you had a "post" model defined in your `factory`, then the generated handlers would have operations like `post`, `createPost`, `updatePosts`, etc.
+You can extend the behavior of collections via _extensions_. The library comes with the following default extensions, but you can always [create your own](#custom-extensions).
-With the request handlers generated and MSW configured, you can query the database using GraphQL API:
+### Default extensions
-```js
-import { gql, useQuery } from '@apollo/client'
+#### `sync()`
-const CREATE_USER = gql`
- query CreateUser($initialValues: UserInput!) {
- createUser(data: $initialValues) {
- firstName
- }
- }
-`
+> [!WARNING]
+> The `sync()` extension is browser-only. It will be ignored in Node.js.
-useQuery(CREATE_USER, {
- variables: {
- initialValues: {
- firstName: 'John',
- },
- },
+Synchronizes collection changes, like creating/updating/deleting records, with the same collection in another browser tab via a `BroadcastChannel`.
+
+```ts
+import { Collection } from '@msw/data'
+import { sync } from '@msw/data/extensions/sync'
+
+const users = new Collection({
+ schema,
+ extensions: [sync()],
})
```
-### Manual integration
+#### `persist()`
-To gain more control over the mocks and implement more complex mocking scenarios (like authentication), consider manual integration of this library with your API mocking solution.
+> [!WARNING]
+> The `persist()` extension is browser-only. It will be ignored in Node.js.
-Take a look at how you can create an entity based on the user's authentication status in a test:
+Persist the records in the collection between page reloads.
-```js
-import { rest } from 'msw'
-import { setupServer } from 'msw/node'
-import { factory, primaryKey } from '@mswjs/data'
+```ts
+import { Collection } from '@msw/data'
+import { persist } from '@msw/data/extensions/persist'
-const db = factory({
- post: {
- id: primaryKey(String),
- title: String,
- },
+const users = new Collection({
+ schema,
+ extensions: [persist()],
})
+```
-const handlers = [
- rest.post('/post', (req, res, cxt) => {
- // Only authenticated users can create new posts.
- if (req.headers.get('authorization') === 'Bearer AUTH_TOKEN') {
- return res(ctx.status(403))
- }
-
- // Create a new entity for the "post" model.
- const newPost = db.post.create(req.body)
-
- // Respond with a mocked response.
- return res(ctx.status(201), ctx.json({ post: newPost }))
- }),
-]
+### Custom extensions
-// Establish requests interception.
-const server = setupServer(...handlers)
-server.listen()
+```ts
+// my-extension.ts
+import { defineExtension } from '@msw/data/extensions'
+
+export function myExtension() {
+ return defineExtension({
+ name: 'my-extension',
+ extend(collection) {
+ // Your logic here.
+ },
+ })
+}
```
-## Honorable mentions
+```ts
+import { Collection } from '@msw/data'
+import { myExtension } from './my-extension.js'
-- [Prisma](https://www.prisma.io) for inspiring the querying client.
-- [Lenz Weber](https://twitter.com/phry) and [Matt Sutkowski](https://twitter.com/de_stroy) for great help with the TypeScript support.
+new Collection({ schema, extensions: [myExtension()] })
+```
diff --git a/commitlint.config.js b/commitlint.config.js
deleted file mode 100644
index 98ee7dfc..00000000
--- a/commitlint.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- extends: ['@commitlint/config-conventional'],
-}
diff --git a/decisions/primary-key.md b/decisions/primary-key.md
new file mode 100644
index 00000000..4cebc418
--- /dev/null
+++ b/decisions/primary-key.md
@@ -0,0 +1,3 @@
+# Primary key
+
+The `primaryKey()` function has been abolished in favor of internal record ID. Every record gets assigned a random immutable UUID upon its creation. The library uses those internal IDs primarily for associating records with each other in relations. The user is free to treat any of the defined model properties as a "primary key", but no such functionality longer exists in the library.
diff --git a/decisions/query-syntax.md b/decisions/query-syntax.md
new file mode 100644
index 00000000..459109a6
--- /dev/null
+++ b/decisions/query-syntax.md
@@ -0,0 +1,52 @@
+# Query syntax
+
+## Query methods
+
+Query methods like `.findFirst()`, `.findMany()`, `.update()`, `.updateMany()`, and others were chosen due to developer familiarity as they might find those (or similar) methods in ORM libraries, like Prisma or Drizzle. I personally like create-find-update-delete terminology.
+
+## Query builder
+
+I went with the query builder pattern over something like `{ OR, AND }` key syntax you might find in Prisma because I dislike mixing record properties and logical properties.
+
+```ts
+users.findFirst({
+ where: {
+ OR: [{ id: 2 }, { name: 'Bob', AND: [{ subscribed: true }] }],
+ },
+})
+```
+
+Mixing record properties and logical properties results in queries that are hard to read. Compare this to the builder syntax that wraps predicates in logical expressions instead:
+
+```ts
+users.findFirst((q) =>
+ q.where({ id: 2 }).or(q.where({ name: 'Bob', subscribed: true })),
+)
+```
+
+Query predicates always include only the record properties, which makes them easy to write. They can be abstracted and composed in any arrangement of logical `q.or` or `q.and` sequences. This composition also allows for mix-and-matching of syntax with zero additional handling on the library's side:
+
+```ts
+users.findFirst((q) => q.or(q.where({ id: 2 }), q.where({ name: 'Bob' })))
+```
+
+## Convenience keys
+
+Convenience keys like `equals`, `in`, `lg`, `notContains`, and others were dropped in favor of function predicates. Functions are infinitely more powerful and the internal overhead of differentiating between reserved convenience keys and user-provided keys is not worth whatever little brevity gained as a result.
+
+You can describe custom logic in your query predicates since any field (or the entire record) can be described as a function.
+
+```ts
+users.findFirst((q) =>
+ q.where({
+ id: (id) => isList.includes(id), // vs { in: idList }
+ }),
+)
+
+users.findFirst((q) =>
+ q.where((user) => {
+ // Custom predicate accepting the entire record.
+ return hasRole('admin', user)
+ }),
+)
+```
diff --git a/decisions/standard-schema.md b/decisions/standard-schema.md
new file mode 100644
index 00000000..c879cc3e
--- /dev/null
+++ b/decisions/standard-schema.md
@@ -0,0 +1,39 @@
+# Standard Schema
+
+I chose to adopt [Standard Schema](https://standardschema.dev/) as the way to define models. This library was never meant to have a custom modeling syntax you have to learn. With the introduction of Standard Schema, it can now rely on any standard-compliant modeling library as the input, making it easier for developers to adopt.
+
+```ts
+import { Collection } from '@msw/data'
+import { z } from 'zod'
+
+const users = new Collection({
+ schema: z.object({
+ id: z.number(),
+ name: z.string().optional(),
+ }),
+})
+```
+
+> Above, Zod is used to model the `users` collection. You can use any of the Standard Schema-compatible libraries instead.
+
+This decision also allows the library to offload custom features, like derived properties, to the schema libraries. For example, to derive a model's property from another property, you follow your schema library's best practices.
+
+```ts
+new Collection({
+ schema: z
+ .object({
+ name: z.string(),
+ email: z.email(),
+ })
+ .transform((user) => {
+ user.email = `${user.name.toLowerCase()}@email.com`
+ return user
+ }),
+})
+```
+
+> Above, I'm using Zod's `.transform()` method to derive the `email` field from the `name` field.
+
+## Model restrictions
+
+In order to support internal IDs and relations, user-defined models _must be either objects or arrays_.
diff --git a/extensions/package.json b/extensions/package.json
new file mode 100644
index 00000000..b855b3a4
--- /dev/null
+++ b/extensions/package.json
@@ -0,0 +1,15 @@
+{
+ "type": "module",
+ "main": "./../build/extensions/index.js",
+ "types": "./../build/extensions/index.d.ts",
+ "exports": {
+ "./sync": {
+ "types": "./../build/extensions/sync.d.ts",
+ "default": "./../build/extensions/sync.js"
+ },
+ "./persist": {
+ "types": "./../build/extensions/persist.d.ts",
+ "default": "./../build/extensions/persist.js"
+ }
+ }
+}
diff --git a/logo.svg b/logo.svg
index f66e60e3..30f32b6d 100644
--- a/logo.svg
+++ b/logo.svg
@@ -26,4 +26,4 @@
-
\ No newline at end of file
+
diff --git a/ossjs.release.config.js b/ossjs.release.config.js
deleted file mode 100644
index 3f582643..00000000
--- a/ossjs.release.config.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- script: 'yarn publish --new-version $RELEASE_VERSION',
-}
diff --git a/package.json b/package.json
index 71d90cfb..8968c5ef 100644
--- a/package.json
+++ b/package.json
@@ -1,69 +1,63 @@
{
- "name": "@mswjs/data",
- "description": "Data modeling and relation library for testing JavaScript applications.",
- "version": "0.10.1",
- "main": "lib/index.js",
- "typings": "lib/index.d.ts",
- "author": "Artem Zakharchenko",
- "license": "MIT",
+ "type": "module",
+ "name": "@msw/data",
+ "version": "1.1.7",
+ "description": "Data querying library for testing JavaScript applications.",
"scripts": {
- "start": "tsc -w",
- "format": "prettier src/**/*.ts --write",
- "test": "jest -c test/jest.config.ts",
- "test:ts": "tsc -p test/tsconfig.test-d.json",
- "clean": "rimraf ./lib",
- "build": "yarn clean && tsc",
- "release": "release publish",
- "prepare": "yarn simple-git-hooks init",
- "prepublishOnly": "yarn build && yarn test:ts && yarn test"
+ "dev": "tsdown -w",
+ "test:node": "vitest",
+ "test:browser": "playwright test",
+ "lint": "publint",
+ "build": "tsdown",
+ "release": "release publish"
+ },
+ "imports": {
+ "#/src/*": "./src/*"
+ },
+ "exports": {
+ ".": "./build/index.mjs",
+ "./extensions": "./build/extensions/index.mjs",
+ "./extensions/sync": "./build/extensions/sync.mjs",
+ "./extensions/persist": "./build/extensions/persist.mjs"
},
"files": [
- "lib",
- "README.md"
+ "./build",
+ "./src",
+ "./tests"
+ ],
+ "keywords": [
+ "data",
+ "query",
+ "mock",
+ "model",
+ "testing",
+ "fake",
+ "msw"
],
+ "license": "MIT",
+ "author": "Artem Zakharchenko ",
+ "homepage": "https://github.com/mswjs/data",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/mswjs/data.git"
+ },
"devDependencies": {
- "@commitlint/cli": "^16.0.1",
- "@commitlint/config-conventional": "^16.0.0",
- "@ossjs/release": "^0.2.1",
- "@types/debug": "^4.1.5",
- "@types/faker": "^5.5.3",
- "@types/jest": "^26.0.22",
- "@types/node-fetch": "^2.5.10",
- "commitizen": "^4.2.4",
- "cz-conventional-changelog": "3.3.0",
- "faker": "^5.5.3",
- "jest": "^26.6.0",
- "msw": "latest",
- "node-fetch": "^2.6.1",
- "page-with": "^0.4.1",
- "prettier": "^2.2.1",
- "rimraf": "^3.0.2",
- "simple-git-hooks": "^2.7.0",
- "ts-jest": "^26.5.5",
- "ts-node": "^9.1.1",
- "typescript": "4.3.5"
+ "@ossjs/release": "^0.10.1",
+ "@playwright/test": "^1.59.1",
+ "@types/node": "^24.6.2",
+ "prettier": "^3.8.3",
+ "publint": "^0.3.18",
+ "tsdown": "^0.21.9",
+ "typescript": "^5.9.3",
+ "vite": "^8.0.8",
+ "vitest": "^4.1.4",
+ "zod": "^4.3.6"
},
"dependencies": {
- "@types/lodash": "^4.14.172",
- "@types/md5": "^2.3.0",
- "@types/pluralize": "^0.0.29",
- "@types/uuid": "^8.3.0",
- "date-fns": "^2.21.1",
- "debug": "^4.3.1",
- "graphql": "^15.5.0",
- "lodash": "^4.17.21",
- "md5": "^2.3.0",
- "outvariant": "^1.2.1",
- "pluralize": "^8.0.0",
- "strict-event-emitter": "^0.2.0",
- "uuid": "^8.3.1"
- },
- "optionalDependencies": {
- "msw": "latest"
- },
- "config": {
- "commitizen": {
- "path": "./node_modules/cz-conventional-changelog"
- }
+ "@standard-schema/spec": "^1.1.0",
+ "es-toolkit": "^1.45.1",
+ "mutative": "^1.3.0",
+ "outvariant": "^1.4.3",
+ "rettime": "^0.11.8"
}
-}
+}
\ No newline at end of file
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..e588d7d7
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig, devices } from '@playwright/test'
+
+export default defineConfig({
+ testDir: './tests',
+ testMatch: '**/*.browser.test.ts',
+ workers: 1,
+ projects: [
+ {
+ use: devices['Desktop Chrome'],
+ },
+ ],
+})
diff --git a/playwright.extend.ts b/playwright.extend.ts
new file mode 100644
index 00000000..33c26af9
--- /dev/null
+++ b/playwright.extend.ts
@@ -0,0 +1,91 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import { invariant } from 'outvariant'
+import { test as testBase, expect, type Page } from '@playwright/test'
+import { createServer, type ViteDevServer } from 'vite'
+
+interface Fixtures {
+ serve: >(
+ fn: () => Promise,
+ ) => Promise<{
+ url: URL
+ evaluate: any>(
+ callback: Callback,
+ options?: { page: Page },
+ ) => Promise>
+ }>
+}
+
+export { expect }
+
+export const test = testBase.extend({
+ async serve({ page }, use) {
+ let server: ViteDevServer | undefined
+
+ const directory = path.join(
+ process.cwd(),
+ './tests/.tmp',
+ test.info().testId,
+ )
+ const entryPath = path.join(directory, 'entry.ts')
+
+ await use(async (fn) => {
+ await fs.promises.mkdir(directory, { recursive: true })
+ await fs.promises.writeFile(
+ entryPath,
+ `window.__vite_playwright_context__ = await (${fn.toString()})();`,
+ 'utf8',
+ )
+
+ await fs.promises.writeFile(
+ path.join(directory, 'index.html'),
+ `
+
+
+
+
+
+
+ `,
+ 'utf8',
+ )
+
+ server = await createServer({
+ root: directory,
+ optimizeDeps: {
+ entries: [entryPath],
+ },
+ build: {
+ target: 'chrome139',
+ rollupOptions: {
+ external: /.+/,
+ },
+ },
+ configFile: false,
+ logLevel: 'error',
+ })
+
+ await server.listen()
+ await page.waitForLoadState('networkidle')
+
+ const url = server.resolvedUrls?.local[0]
+ invariant(url, 'Failed to spawn Vite dev server')
+
+ return {
+ url: new URL(url),
+ async evaluate(callback, options) {
+ const targetPage = options?.page || page
+ const context = await targetPage.evaluateHandle(
+ () => window['__vite_playwright_context__' as keyof typeof window],
+ )
+ return await context.evaluate(callback)
+ },
+ }
+ })
+
+ await Promise.all([
+ server?.close(),
+ fs.promises.rm(directory, { recursive: true }),
+ ])
+ },
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 00000000..5c5f74f5
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,2156 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@standard-schema/spec':
+ specifier: ^1.1.0
+ version: 1.1.0
+ es-toolkit:
+ specifier: ^1.45.1
+ version: 1.45.1
+ mutative:
+ specifier: ^1.3.0
+ version: 1.3.0
+ outvariant:
+ specifier: ^1.4.3
+ version: 1.4.3
+ rettime:
+ specifier: ^0.11.8
+ version: 0.11.8
+ devDependencies:
+ '@ossjs/release':
+ specifier: ^0.10.1
+ version: 0.10.1
+ '@playwright/test':
+ specifier: ^1.59.1
+ version: 1.59.1
+ '@types/node':
+ specifier: ^24.6.2
+ version: 24.6.2
+ prettier:
+ specifier: ^3.8.3
+ version: 3.8.3
+ publint:
+ specifier: ^0.3.18
+ version: 0.3.18
+ tsdown:
+ specifier: ^0.21.9
+ version: 0.21.9(publint@0.3.18)(typescript@5.9.3)
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^8.0.8
+ version: 8.0.8(@types/node@24.6.2)(jiti@2.5.1)
+ vitest:
+ specifier: ^4.1.4
+ version: 4.1.4(@types/node@24.6.2)(vite@8.0.8(@types/node@24.6.2)(jiti@2.5.1))
+ zod:
+ specifier: ^4.3.6
+ version: 4.3.6
+
+packages:
+
+ '@babel/generator@8.0.0-rc.3':
+ resolution: {integrity: sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ '@babel/helper-string-parser@8.0.0-rc.3':
+ resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ '@babel/helper-validator-identifier@8.0.0-rc.3':
+ resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ '@babel/parser@8.0.0-rc.3':
+ resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ '@babel/types@8.0.0-rc.3':
+ resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ '@emnapi/core@1.9.2':
+ resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==}
+
+ '@emnapi/runtime@1.9.2':
+ resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==}
+
+ '@emnapi/wasi-threads@1.2.1':
+ resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
+
+ '@jridgewell/gen-mapping@0.3.13':
+ resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.5':
+ resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+
+ '@napi-rs/wasm-runtime@1.1.4':
+ resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@open-draft/deferred-promise@2.2.0':
+ resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==}
+
+ '@ossjs/release@0.10.1':
+ resolution: {integrity: sha512-Djr1DXVTeR476ZdNC0RYPQQuX7Hx+CnEex1AyBBnsizQ7KqsidW99iooDFxT36jy8ReXwdTgIsjP2OSdyjfbLQ==}
+ engines: {node: '>=20.0.0'}
+ hasBin: true
+
+ '@oxc-project/types@0.124.0':
+ resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==}
+
+ '@oxc-project/types@0.126.0':
+ resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==}
+
+ '@playwright/test@1.59.1':
+ resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ '@pnpm/config.env-replace@1.1.0':
+ resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==}
+ engines: {node: '>=12.22.0'}
+
+ '@pnpm/network.ca-file@1.0.2':
+ resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==}
+ engines: {node: '>=12.22.0'}
+
+ '@pnpm/npm-conf@2.3.1':
+ resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==}
+ engines: {node: '>=12'}
+
+ '@publint/pack@0.1.4':
+ resolution: {integrity: sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==}
+ engines: {node: '>=18'}
+
+ '@quansync/fs@1.0.0':
+ resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
+
+ '@rolldown/binding-android-arm64@1.0.0-rc.15':
+ resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-android-arm64@1.0.0-rc.16':
+ resolution: {integrity: sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.15':
+ resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.16':
+ resolution: {integrity: sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.15':
+ resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.16':
+ resolution: {integrity: sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.15':
+ resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.16':
+ resolution: {integrity: sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
+ resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16':
+ resolution: {integrity: sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
+ resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16':
+ resolution: {integrity: sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
+ resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16':
+ resolution: {integrity: sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
+ resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16':
+ resolution: {integrity: sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
+ resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16':
+ resolution: {integrity: sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
+ resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16':
+ resolution: {integrity: sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
+ resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.16':
+ resolution: {integrity: sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
+ resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.16':
+ resolution: {integrity: sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
+ resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.16':
+ resolution: {integrity: sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
+ resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16':
+ resolution: {integrity: sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
+ resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16':
+ resolution: {integrity: sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.0-rc.15':
+ resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==}
+
+ '@rolldown/pluginutils@1.0.0-rc.16':
+ resolution: {integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==}
+
+ '@simple-libs/stream-utils@1.2.0':
+ resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==}
+ engines: {node: '>=18'}
+
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
+
+ '@tybys/wasm-util@0.10.1':
+ resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
+
+ '@types/chai@5.2.2':
+ resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
+
+ '@types/conventional-commits-parser@5.0.2':
+ resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==}
+
+ '@types/deep-eql@4.0.2':
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
+
+ '@types/estree@1.0.8':
+ resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+ '@types/issue-parser@3.0.5':
+ resolution: {integrity: sha512-fvOrnb7uS6qRme16tfyxy9SjOgx47Krkt/ilLS7axP3SWtJb9GZlduWX2bAsJOnr1HuCwJh88rCidzCZ1LwuZg==}
+
+ '@types/jsesc@2.5.1':
+ resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==}
+
+ '@types/node@24.12.2':
+ resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==}
+
+ '@types/node@24.6.2':
+ resolution: {integrity: sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==}
+
+ '@types/semver@7.7.1':
+ resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
+
+ '@types/yargs-parser@21.0.3':
+ resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
+
+ '@types/yargs@17.0.35':
+ resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
+
+ '@vitest/expect@4.1.4':
+ resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==}
+
+ '@vitest/mocker@4.1.4':
+ resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==}
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+
+ '@vitest/pretty-format@4.1.4':
+ resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==}
+
+ '@vitest/runner@4.1.4':
+ resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==}
+
+ '@vitest/snapshot@4.1.4':
+ resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==}
+
+ '@vitest/spy@4.1.4':
+ resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==}
+
+ '@vitest/utils@4.1.4':
+ resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==}
+
+ ajv@8.17.1:
+ resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
+
+ ansi-regex@6.2.2:
+ resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
+ engines: {node: '>=12'}
+
+ ansi-styles@3.2.1:
+ resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
+ engines: {node: '>=4'}
+
+ ansi-styles@6.2.3:
+ resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
+ engines: {node: '>=12'}
+
+ ansis@4.2.0:
+ resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
+ engines: {node: '>=14'}
+
+ args@5.0.3:
+ resolution: {integrity: sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA==}
+ engines: {node: '>= 6.0.0'}
+
+ argv-formatter@1.0.0:
+ resolution: {integrity: sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==}
+
+ ast-kit@3.0.0-beta.1:
+ resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==}
+ engines: {node: '>=20.19.0'}
+
+ atomic-sleep@1.0.0:
+ resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
+ engines: {node: '>=8.0.0'}
+
+ birpc@4.0.0:
+ resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==}
+
+ cac@7.0.0:
+ resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
+ engines: {node: '>=20.19.0'}
+
+ camelcase@5.0.0:
+ resolution: {integrity: sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==}
+ engines: {node: '>=6'}
+
+ chai@6.2.2:
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
+ engines: {node: '>=18'}
+
+ chalk@2.4.2:
+ resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
+ engines: {node: '>=4'}
+
+ cliui@9.0.1:
+ resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
+ engines: {node: '>=20'}
+
+ color-convert@1.9.3:
+ resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
+
+ color-name@1.1.3:
+ resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
+
+ colorette@2.0.20:
+ resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
+
+ config-chain@1.1.13:
+ resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
+
+ conventional-commits-parser@6.4.0:
+ resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ core-util-is@1.0.3:
+ resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+
+ dateformat@4.6.3:
+ resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
+
+ deep-extend@0.6.0:
+ resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
+ engines: {node: '>=4.0.0'}
+
+ defu@6.1.7:
+ resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ dts-resolver@2.1.3:
+ resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ oxc-resolver: '>=11.0.0'
+ peerDependenciesMeta:
+ oxc-resolver:
+ optional: true
+
+ duplexer2@0.1.4:
+ resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
+
+ duplexify@4.1.3:
+ resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
+
+ emoji-regex@10.5.0:
+ resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==}
+
+ empathic@2.0.0:
+ resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
+ engines: {node: '>=14'}
+
+ end-of-stream@1.4.5:
+ resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+
+ es-module-lexer@2.0.0:
+ resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
+
+ es-toolkit@1.45.1:
+ resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==}
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-string-regexp@1.0.5:
+ resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+ engines: {node: '>=0.8.0'}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ expect-type@1.3.0:
+ resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
+ engines: {node: '>=12.0.0'}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-redact@3.5.0:
+ resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
+ engines: {node: '>=6'}
+
+ fast-safe-stringify@2.1.1:
+ resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
+
+ fast-uri@3.1.0:
+ resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-east-asian-width@1.4.0:
+ resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
+ engines: {node: '>=18'}
+
+ get-stream@6.0.1:
+ resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
+ engines: {node: '>=10'}
+
+ get-tsconfig@4.14.0:
+ resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
+
+ git-log-parser@1.2.1:
+ resolution: {integrity: sha512-PI+sPDvHXNPl5WNOErAK05s3j0lgwUzMN6o8cyQrDaKfT3qd7TmNJKeXX+SknI5I0QhG5fVPAEwSY4tRGDtYoQ==}
+
+ graceful-fs@4.2.10:
+ resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==}
+
+ has-flag@3.0.0:
+ resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
+ engines: {node: '>=4'}
+
+ hookable@6.1.1:
+ resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==}
+
+ import-without-cache@0.3.3:
+ resolution: {integrity: sha512-bDxwDdF04gm550DfZHgffvlX+9kUlcz32UD0AeBTmVPFiWkrexF2XVmiuFFbDhiFuP8fQkrkvI2KdSNPYWAXkQ==}
+ engines: {node: '>=20.19.0'}
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+
+ isarray@1.0.0:
+ resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+
+ issue-parser@7.0.1:
+ resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==}
+ engines: {node: ^18.17 || >=20.6.1}
+
+ jiti@2.5.1:
+ resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
+ hasBin: true
+
+ joycon@3.1.1:
+ resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
+ engines: {node: '>=10'}
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ leven@2.1.0:
+ resolution: {integrity: sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==}
+ engines: {node: '>=0.10.0'}
+
+ lightningcss-android-arm64@1.32.0:
+ resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.32.0:
+ resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.32.0:
+ resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.32.0:
+ resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-linux-x64-musl@1.32.0:
+ resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.32.0:
+ resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
+ engines: {node: '>= 12.0.0'}
+
+ lodash.capitalize@4.2.1:
+ resolution: {integrity: sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==}
+
+ lodash.escaperegexp@4.1.2:
+ resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
+
+ lodash.isplainobject@4.0.6:
+ resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
+
+ lodash.isstring@4.0.1:
+ resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
+
+ lodash.uniqby@4.7.0:
+ resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==}
+
+ magic-string@0.30.21:
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
+
+ meow@13.2.0:
+ resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
+ engines: {node: '>=18'}
+
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+ mri@1.1.4:
+ resolution: {integrity: sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==}
+ engines: {node: '>=4'}
+
+ mri@1.2.0:
+ resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
+ engines: {node: '>=4'}
+
+ mutative@1.3.0:
+ resolution: {integrity: sha512-8MJj6URmOZAV70dpFe1YnSppRTKC4DsMkXQiBDFayLcDI4ljGokHxmpqaBQuDWa4iAxWaJJ1PS8vAmbntjjKmQ==}
+ engines: {node: '>=14.0'}
+
+ nanoid@3.3.11:
+ resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ obug@2.1.1:
+ resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
+
+ on-exit-leak-free@0.2.0:
+ resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ outvariant@1.4.3:
+ resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
+
+ package-manager-detector@1.6.0:
+ resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
+ picomatch@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ pino-abstract-transport@0.5.0:
+ resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==}
+
+ pino-pretty@7.6.1:
+ resolution: {integrity: sha512-H7N6ZYkiyrfwBGW9CSjx0uyO9Q2Lyt73881+OTYk8v3TiTdgN92QHrWlEq/LeWw5XtDP64jeSk3mnc6T+xX9/w==}
+ hasBin: true
+
+ pino-std-serializers@4.0.0:
+ resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==}
+
+ pino@7.11.0:
+ resolution: {integrity: sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==}
+ hasBin: true
+
+ playwright-core@1.59.1:
+ resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.59.1:
+ resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ postcss@8.5.10:
+ resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prettier@3.8.3:
+ resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ process-nextick-args@2.0.1:
+ resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+
+ process-warning@1.0.0:
+ resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==}
+
+ proto-list@1.2.4:
+ resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
+
+ publint@0.3.18:
+ resolution: {integrity: sha512-JRJFeBTrfx4qLwEuGFPk+haJOJN97KnPuK01yj+4k/Wj5BgoOK5uNsivporiqBjk2JDaslg7qJOhGRnpltGeog==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ pump@3.0.3:
+ resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
+
+ quansync@1.0.0:
+ resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
+
+ quick-format-unescaped@4.0.4:
+ resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
+
+ rc@1.2.8:
+ resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
+ hasBin: true
+
+ readable-stream@2.3.8:
+ resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+
+ readable-stream@3.6.2:
+ resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+ engines: {node: '>= 6'}
+
+ real-require@0.1.0:
+ resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==}
+ engines: {node: '>= 12.13.0'}
+
+ registry-auth-token@5.1.0:
+ resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==}
+ engines: {node: '>=14'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+ rettime@0.11.8:
+ resolution: {integrity: sha512-0fERGXktJTyJ+h8fBEiPxHPEFOu0h15JY7JtwrOVqR5K+vb99ho6IyOo7ekLS3h4sJCzIDy4VWKIbZUfe9njmg==}
+
+ rfdc@1.4.1:
+ resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+
+ rolldown-plugin-dts@0.23.2:
+ resolution: {integrity: sha512-PbSqLawLgZBGcOGT3yqWBGn4cX+wh2nt5FuBGdcMHyOhoukmjbhYAl8NT9sE4U38Cm9tqLOIQeOrvzeayM0DLQ==}
+ engines: {node: '>=20.19.0'}
+ peerDependencies:
+ '@ts-macro/tsc': ^0.3.6
+ '@typescript/native-preview': '>=7.0.0-dev.20260325.1'
+ rolldown: ^1.0.0-rc.12
+ typescript: ^5.0.0 || ^6.0.0
+ vue-tsc: ~3.2.0
+ peerDependenciesMeta:
+ '@ts-macro/tsc':
+ optional: true
+ '@typescript/native-preview':
+ optional: true
+ typescript:
+ optional: true
+ vue-tsc:
+ optional: true
+
+ rolldown@1.0.0-rc.15:
+ resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ rolldown@1.0.0-rc.16:
+ resolution: {integrity: sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ sade@1.8.1:
+ resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
+ engines: {node: '>=6'}
+
+ safe-buffer@5.1.2:
+ resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
+ safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+ safe-stable-stringify@2.5.0:
+ resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
+ engines: {node: '>=10'}
+
+ secure-json-parse@2.7.0:
+ resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
+
+ semver@7.7.4:
+ resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ sonic-boom@2.8.0:
+ resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ spawn-error-forwarder@1.0.0:
+ resolution: {integrity: sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==}
+
+ split2@1.0.0:
+ resolution: {integrity: sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==}
+
+ split2@4.2.0:
+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+ engines: {node: '>= 10.x'}
+
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
+ std-env@4.1.0:
+ resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==}
+
+ stream-combiner2@1.1.1:
+ resolution: {integrity: sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==}
+
+ stream-shift@1.0.3:
+ resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
+
+ string-width@7.2.0:
+ resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+ engines: {node: '>=18'}
+
+ string_decoder@1.1.1:
+ resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+
+ string_decoder@1.3.0:
+ resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+
+ strip-ansi@7.1.2:
+ resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
+ engines: {node: '>=12'}
+
+ strip-json-comments@2.0.1:
+ resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
+ engines: {node: '>=0.10.0'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ supports-color@5.5.0:
+ resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
+ engines: {node: '>=4'}
+
+ thread-stream@0.15.2:
+ resolution: {integrity: sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==}
+
+ through2@2.0.5:
+ resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
+
+ tinybench@2.9.0:
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
+
+ tinyexec@1.1.1:
+ resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.15:
+ resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+ engines: {node: '>=12.0.0'}
+
+ tinyglobby@0.2.16:
+ resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
+ engines: {node: '>=12.0.0'}
+
+ tinyrainbow@3.1.0:
+ resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
+ engines: {node: '>=14.0.0'}
+
+ traverse@0.6.8:
+ resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==}
+ engines: {node: '>= 0.4'}
+
+ tree-kill@1.2.2:
+ resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
+ hasBin: true
+
+ tsdown@0.21.9:
+ resolution: {integrity: sha512-tZPv2zMaMnjj9H9h0SDqpSXa9YWVZWHlG46DnSgNTFX6aq001MSI8kuBzJumr/u099nWj+1v5S7rhbnHk5jCHA==}
+ engines: {node: '>=20.19.0'}
+ hasBin: true
+ peerDependencies:
+ '@arethetypeswrong/core': ^0.18.1
+ '@tsdown/css': 0.21.9
+ '@tsdown/exe': 0.21.9
+ '@vitejs/devtools': '*'
+ publint: ^0.3.0
+ typescript: ^5.0.0 || ^6.0.0
+ unplugin-unused: ^0.5.0
+ peerDependenciesMeta:
+ '@arethetypeswrong/core':
+ optional: true
+ '@tsdown/css':
+ optional: true
+ '@tsdown/exe':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ publint:
+ optional: true
+ typescript:
+ optional: true
+ unplugin-unused:
+ optional: true
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ unconfig-core@7.5.0:
+ resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==}
+
+ undici-types@7.13.0:
+ resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==}
+
+ undici-types@7.16.0:
+ resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+ unrun@0.2.36:
+ resolution: {integrity: sha512-ICAGv44LHSKjCdI4B4rk99lJLHXBweutO4MUwu3cavMlYtXID0Tn5e1Kwe/Uj6BSAuHHXfi1JheFVCYhcXHfAg==}
+ engines: {node: '>=20.19.0'}
+ hasBin: true
+ peerDependencies:
+ synckit: ^0.11.11
+ peerDependenciesMeta:
+ synckit:
+ optional: true
+
+ until-async@3.0.2:
+ resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ vite@8.0.8:
+ resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.1.0
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitest@4.1.4:
+ resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==}
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@opentelemetry/api': ^1.9.0
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
+ '@vitest/browser-playwright': 4.1.4
+ '@vitest/browser-preview': 4.1.4
+ '@vitest/browser-webdriverio': 4.1.4
+ '@vitest/coverage-istanbul': 4.1.4
+ '@vitest/coverage-v8': 4.1.4
+ '@vitest/ui': 4.1.4
+ happy-dom: '*'
+ jsdom: '*'
+ vite: ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@opentelemetry/api':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser-playwright':
+ optional: true
+ '@vitest/browser-preview':
+ optional: true
+ '@vitest/browser-webdriverio':
+ optional: true
+ '@vitest/coverage-istanbul':
+ optional: true
+ '@vitest/coverage-v8':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
+ why-is-node-running@2.3.0:
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
+ engines: {node: '>=8'}
+ hasBin: true
+
+ wrap-ansi@9.0.2:
+ resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
+ engines: {node: '>=18'}
+
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ xtend@4.0.2:
+ resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
+ engines: {node: '>=0.4'}
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
+ yargs-parser@22.0.0:
+ resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
+ yargs@18.0.0:
+ resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
+ zod@4.3.6:
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+
+snapshots:
+
+ '@babel/generator@8.0.0-rc.3':
+ dependencies:
+ '@babel/parser': 8.0.0-rc.3
+ '@babel/types': 8.0.0-rc.3
+ '@jridgewell/gen-mapping': 0.3.13
+ '@jridgewell/trace-mapping': 0.3.31
+ '@types/jsesc': 2.5.1
+ jsesc: 3.1.0
+
+ '@babel/helper-string-parser@8.0.0-rc.3': {}
+
+ '@babel/helper-validator-identifier@8.0.0-rc.3': {}
+
+ '@babel/parser@8.0.0-rc.3':
+ dependencies:
+ '@babel/types': 8.0.0-rc.3
+
+ '@babel/types@8.0.0-rc.3':
+ dependencies:
+ '@babel/helper-string-parser': 8.0.0-rc.3
+ '@babel/helper-validator-identifier': 8.0.0-rc.3
+
+ '@emnapi/core@1.9.2':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.1
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.9.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.13':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping': 0.3.31
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.5': {}
+
+ '@jridgewell/trace-mapping@0.3.31':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)':
+ dependencies:
+ '@emnapi/core': 1.9.2
+ '@emnapi/runtime': 1.9.2
+ '@tybys/wasm-util': 0.10.1
+ optional: true
+
+ '@open-draft/deferred-promise@2.2.0': {}
+
+ '@ossjs/release@0.10.1':
+ dependencies:
+ '@open-draft/deferred-promise': 2.2.0
+ '@types/conventional-commits-parser': 5.0.2
+ '@types/issue-parser': 3.0.5
+ '@types/node': 24.12.2
+ '@types/semver': 7.7.1
+ '@types/yargs': 17.0.35
+ ajv: 8.17.1
+ conventional-commits-parser: 6.4.0
+ get-stream: 6.0.1
+ git-log-parser: 1.2.1
+ issue-parser: 7.0.1
+ outvariant: 1.4.3
+ pino: 7.11.0
+ pino-pretty: 7.6.1
+ publint: 0.3.18
+ rc: 1.2.8
+ registry-auth-token: 5.1.0
+ semver: 7.7.4
+ until-async: 3.0.2
+ yargs: 18.0.0
+
+ '@oxc-project/types@0.124.0': {}
+
+ '@oxc-project/types@0.126.0': {}
+
+ '@playwright/test@1.59.1':
+ dependencies:
+ playwright: 1.59.1
+
+ '@pnpm/config.env-replace@1.1.0': {}
+
+ '@pnpm/network.ca-file@1.0.2':
+ dependencies:
+ graceful-fs: 4.2.10
+
+ '@pnpm/npm-conf@2.3.1':
+ dependencies:
+ '@pnpm/config.env-replace': 1.1.0
+ '@pnpm/network.ca-file': 1.0.2
+ config-chain: 1.1.13
+
+ '@publint/pack@0.1.4': {}
+
+ '@quansync/fs@1.0.0':
+ dependencies:
+ quansync: 1.0.0
+
+ '@rolldown/binding-android-arm64@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-android-arm64@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.15':
+ dependencies:
+ '@emnapi/core': 1.9.2
+ '@emnapi/runtime': 1.9.2
+ '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.0.0-rc.16':
+ dependencies:
+ '@emnapi/core': 1.9.2
+ '@emnapi/runtime': 1.9.2
+ '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.0-rc.15': {}
+
+ '@rolldown/pluginutils@1.0.0-rc.16': {}
+
+ '@simple-libs/stream-utils@1.2.0': {}
+
+ '@standard-schema/spec@1.1.0': {}
+
+ '@tybys/wasm-util@0.10.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/chai@5.2.2':
+ dependencies:
+ '@types/deep-eql': 4.0.2
+
+ '@types/conventional-commits-parser@5.0.2':
+ dependencies:
+ '@types/node': 24.12.2
+
+ '@types/deep-eql@4.0.2': {}
+
+ '@types/estree@1.0.8': {}
+
+ '@types/issue-parser@3.0.5': {}
+
+ '@types/jsesc@2.5.1': {}
+
+ '@types/node@24.12.2':
+ dependencies:
+ undici-types: 7.16.0
+
+ '@types/node@24.6.2':
+ dependencies:
+ undici-types: 7.13.0
+
+ '@types/semver@7.7.1': {}
+
+ '@types/yargs-parser@21.0.3': {}
+
+ '@types/yargs@17.0.35':
+ dependencies:
+ '@types/yargs-parser': 21.0.3
+
+ '@vitest/expect@4.1.4':
+ dependencies:
+ '@standard-schema/spec': 1.1.0
+ '@types/chai': 5.2.2
+ '@vitest/spy': 4.1.4
+ '@vitest/utils': 4.1.4
+ chai: 6.2.2
+ tinyrainbow: 3.1.0
+
+ '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@24.6.2)(jiti@2.5.1))':
+ dependencies:
+ '@vitest/spy': 4.1.4
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 8.0.8(@types/node@24.6.2)(jiti@2.5.1)
+
+ '@vitest/pretty-format@4.1.4':
+ dependencies:
+ tinyrainbow: 3.1.0
+
+ '@vitest/runner@4.1.4':
+ dependencies:
+ '@vitest/utils': 4.1.4
+ pathe: 2.0.3
+
+ '@vitest/snapshot@4.1.4':
+ dependencies:
+ '@vitest/pretty-format': 4.1.4
+ '@vitest/utils': 4.1.4
+ magic-string: 0.30.21
+ pathe: 2.0.3
+
+ '@vitest/spy@4.1.4': {}
+
+ '@vitest/utils@4.1.4':
+ dependencies:
+ '@vitest/pretty-format': 4.1.4
+ convert-source-map: 2.0.0
+ tinyrainbow: 3.1.0
+
+ ajv@8.17.1:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.1.0
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ ansi-regex@6.2.2: {}
+
+ ansi-styles@3.2.1:
+ dependencies:
+ color-convert: 1.9.3
+
+ ansi-styles@6.2.3: {}
+
+ ansis@4.2.0: {}
+
+ args@5.0.3:
+ dependencies:
+ camelcase: 5.0.0
+ chalk: 2.4.2
+ leven: 2.1.0
+ mri: 1.1.4
+
+ argv-formatter@1.0.0: {}
+
+ ast-kit@3.0.0-beta.1:
+ dependencies:
+ '@babel/parser': 8.0.0-rc.3
+ estree-walker: 3.0.3
+ pathe: 2.0.3
+
+ atomic-sleep@1.0.0: {}
+
+ birpc@4.0.0: {}
+
+ cac@7.0.0: {}
+
+ camelcase@5.0.0: {}
+
+ chai@6.2.2: {}
+
+ chalk@2.4.2:
+ dependencies:
+ ansi-styles: 3.2.1
+ escape-string-regexp: 1.0.5
+ supports-color: 5.5.0
+
+ cliui@9.0.1:
+ dependencies:
+ string-width: 7.2.0
+ strip-ansi: 7.1.2
+ wrap-ansi: 9.0.2
+
+ color-convert@1.9.3:
+ dependencies:
+ color-name: 1.1.3
+
+ color-name@1.1.3: {}
+
+ colorette@2.0.20: {}
+
+ config-chain@1.1.13:
+ dependencies:
+ ini: 1.3.8
+ proto-list: 1.2.4
+
+ conventional-commits-parser@6.4.0:
+ dependencies:
+ '@simple-libs/stream-utils': 1.2.0
+ meow: 13.2.0
+
+ convert-source-map@2.0.0: {}
+
+ core-util-is@1.0.3: {}
+
+ dateformat@4.6.3: {}
+
+ deep-extend@0.6.0: {}
+
+ defu@6.1.7: {}
+
+ detect-libc@2.1.2: {}
+
+ dts-resolver@2.1.3: {}
+
+ duplexer2@0.1.4:
+ dependencies:
+ readable-stream: 2.3.8
+
+ duplexify@4.1.3:
+ dependencies:
+ end-of-stream: 1.4.5
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+ stream-shift: 1.0.3
+
+ emoji-regex@10.5.0: {}
+
+ empathic@2.0.0: {}
+
+ end-of-stream@1.4.5:
+ dependencies:
+ once: 1.4.0
+
+ es-module-lexer@2.0.0: {}
+
+ es-toolkit@1.45.1: {}
+
+ escalade@3.2.0: {}
+
+ escape-string-regexp@1.0.5: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.8
+
+ expect-type@1.3.0: {}
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-redact@3.5.0: {}
+
+ fast-safe-stringify@2.1.1: {}
+
+ fast-uri@3.1.0: {}
+
+ fdir@6.5.0(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
+ fsevents@2.3.2:
+ optional: true
+
+ fsevents@2.3.3:
+ optional: true
+
+ get-caller-file@2.0.5: {}
+
+ get-east-asian-width@1.4.0: {}
+
+ get-stream@6.0.1: {}
+
+ get-tsconfig@4.14.0:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
+ git-log-parser@1.2.1:
+ dependencies:
+ argv-formatter: 1.0.0
+ spawn-error-forwarder: 1.0.0
+ split2: 1.0.0
+ stream-combiner2: 1.1.1
+ through2: 2.0.5
+ traverse: 0.6.8
+
+ graceful-fs@4.2.10: {}
+
+ has-flag@3.0.0: {}
+
+ hookable@6.1.1: {}
+
+ import-without-cache@0.3.3: {}
+
+ inherits@2.0.4: {}
+
+ ini@1.3.8: {}
+
+ isarray@1.0.0: {}
+
+ issue-parser@7.0.1:
+ dependencies:
+ lodash.capitalize: 4.2.1
+ lodash.escaperegexp: 4.1.2
+ lodash.isplainobject: 4.0.6
+ lodash.isstring: 4.0.1
+ lodash.uniqby: 4.7.0
+
+ jiti@2.5.1:
+ optional: true
+
+ joycon@3.1.1: {}
+
+ jsesc@3.1.0: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ leven@2.1.0: {}
+
+ lightningcss-android-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.32.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.32.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.32.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.32.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.32.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.32.0:
+ optional: true
+
+ lightningcss@1.32.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.32.0
+ lightningcss-darwin-arm64: 1.32.0
+ lightningcss-darwin-x64: 1.32.0
+ lightningcss-freebsd-x64: 1.32.0
+ lightningcss-linux-arm-gnueabihf: 1.32.0
+ lightningcss-linux-arm64-gnu: 1.32.0
+ lightningcss-linux-arm64-musl: 1.32.0
+ lightningcss-linux-x64-gnu: 1.32.0
+ lightningcss-linux-x64-musl: 1.32.0
+ lightningcss-win32-arm64-msvc: 1.32.0
+ lightningcss-win32-x64-msvc: 1.32.0
+
+ lodash.capitalize@4.2.1: {}
+
+ lodash.escaperegexp@4.1.2: {}
+
+ lodash.isplainobject@4.0.6: {}
+
+ lodash.isstring@4.0.1: {}
+
+ lodash.uniqby@4.7.0: {}
+
+ magic-string@0.30.21:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.5
+
+ meow@13.2.0: {}
+
+ minimist@1.2.8: {}
+
+ mri@1.1.4: {}
+
+ mri@1.2.0: {}
+
+ mutative@1.3.0: {}
+
+ nanoid@3.3.11: {}
+
+ obug@2.1.1: {}
+
+ on-exit-leak-free@0.2.0: {}
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ outvariant@1.4.3: {}
+
+ package-manager-detector@1.6.0: {}
+
+ pathe@2.0.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@4.0.3: {}
+
+ picomatch@4.0.4: {}
+
+ pino-abstract-transport@0.5.0:
+ dependencies:
+ duplexify: 4.1.3
+ split2: 4.2.0
+
+ pino-pretty@7.6.1:
+ dependencies:
+ args: 5.0.3
+ colorette: 2.0.20
+ dateformat: 4.6.3
+ fast-safe-stringify: 2.1.1
+ joycon: 3.1.1
+ on-exit-leak-free: 0.2.0
+ pino-abstract-transport: 0.5.0
+ pump: 3.0.3
+ readable-stream: 3.6.2
+ rfdc: 1.4.1
+ secure-json-parse: 2.7.0
+ sonic-boom: 2.8.0
+ strip-json-comments: 3.1.1
+
+ pino-std-serializers@4.0.0: {}
+
+ pino@7.11.0:
+ dependencies:
+ atomic-sleep: 1.0.0
+ fast-redact: 3.5.0
+ on-exit-leak-free: 0.2.0
+ pino-abstract-transport: 0.5.0
+ pino-std-serializers: 4.0.0
+ process-warning: 1.0.0
+ quick-format-unescaped: 4.0.4
+ real-require: 0.1.0
+ safe-stable-stringify: 2.5.0
+ sonic-boom: 2.8.0
+ thread-stream: 0.15.2
+
+ playwright-core@1.59.1: {}
+
+ playwright@1.59.1:
+ dependencies:
+ playwright-core: 1.59.1
+ optionalDependencies:
+ fsevents: 2.3.2
+
+ postcss@8.5.10:
+ dependencies:
+ nanoid: 3.3.11
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prettier@3.8.3: {}
+
+ process-nextick-args@2.0.1: {}
+
+ process-warning@1.0.0: {}
+
+ proto-list@1.2.4: {}
+
+ publint@0.3.18:
+ dependencies:
+ '@publint/pack': 0.1.4
+ package-manager-detector: 1.6.0
+ picocolors: 1.1.1
+ sade: 1.8.1
+
+ pump@3.0.3:
+ dependencies:
+ end-of-stream: 1.4.5
+ once: 1.4.0
+
+ quansync@1.0.0: {}
+
+ quick-format-unescaped@4.0.4: {}
+
+ rc@1.2.8:
+ dependencies:
+ deep-extend: 0.6.0
+ ini: 1.3.8
+ minimist: 1.2.8
+ strip-json-comments: 2.0.1
+
+ readable-stream@2.3.8:
+ dependencies:
+ core-util-is: 1.0.3
+ inherits: 2.0.4
+ isarray: 1.0.0
+ process-nextick-args: 2.0.1
+ safe-buffer: 5.1.2
+ string_decoder: 1.1.1
+ util-deprecate: 1.0.2
+
+ readable-stream@3.6.2:
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+
+ real-require@0.1.0: {}
+
+ registry-auth-token@5.1.0:
+ dependencies:
+ '@pnpm/npm-conf': 2.3.1
+
+ require-from-string@2.0.2: {}
+
+ resolve-pkg-maps@1.0.0: {}
+
+ rettime@0.11.8: {}
+
+ rfdc@1.4.1: {}
+
+ rolldown-plugin-dts@0.23.2(rolldown@1.0.0-rc.16)(typescript@5.9.3):
+ dependencies:
+ '@babel/generator': 8.0.0-rc.3
+ '@babel/helper-validator-identifier': 8.0.0-rc.3
+ '@babel/parser': 8.0.0-rc.3
+ '@babel/types': 8.0.0-rc.3
+ ast-kit: 3.0.0-beta.1
+ birpc: 4.0.0
+ dts-resolver: 2.1.3
+ get-tsconfig: 4.14.0
+ obug: 2.1.1
+ picomatch: 4.0.4
+ rolldown: 1.0.0-rc.16
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - oxc-resolver
+
+ rolldown@1.0.0-rc.15:
+ dependencies:
+ '@oxc-project/types': 0.124.0
+ '@rolldown/pluginutils': 1.0.0-rc.15
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.0-rc.15
+ '@rolldown/binding-darwin-arm64': 1.0.0-rc.15
+ '@rolldown/binding-darwin-x64': 1.0.0-rc.15
+ '@rolldown/binding-freebsd-x64': 1.0.0-rc.15
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15
+ '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15
+ '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15
+ '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15
+ '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15
+
+ rolldown@1.0.0-rc.16:
+ dependencies:
+ '@oxc-project/types': 0.126.0
+ '@rolldown/pluginutils': 1.0.0-rc.16
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.0.0-rc.16
+ '@rolldown/binding-darwin-arm64': 1.0.0-rc.16
+ '@rolldown/binding-darwin-x64': 1.0.0-rc.16
+ '@rolldown/binding-freebsd-x64': 1.0.0-rc.16
+ '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.16
+ '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.16
+ '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.16
+ '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.16
+ '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.16
+ '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.16
+ '@rolldown/binding-linux-x64-musl': 1.0.0-rc.16
+ '@rolldown/binding-openharmony-arm64': 1.0.0-rc.16
+ '@rolldown/binding-wasm32-wasi': 1.0.0-rc.16
+ '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.16
+ '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.16
+
+ sade@1.8.1:
+ dependencies:
+ mri: 1.2.0
+
+ safe-buffer@5.1.2: {}
+
+ safe-buffer@5.2.1: {}
+
+ safe-stable-stringify@2.5.0: {}
+
+ secure-json-parse@2.7.0: {}
+
+ semver@7.7.4: {}
+
+ siginfo@2.0.0: {}
+
+ sonic-boom@2.8.0:
+ dependencies:
+ atomic-sleep: 1.0.0
+
+ source-map-js@1.2.1: {}
+
+ spawn-error-forwarder@1.0.0: {}
+
+ split2@1.0.0:
+ dependencies:
+ through2: 2.0.5
+
+ split2@4.2.0: {}
+
+ stackback@0.0.2: {}
+
+ std-env@4.1.0: {}
+
+ stream-combiner2@1.1.1:
+ dependencies:
+ duplexer2: 0.1.4
+ readable-stream: 2.3.8
+
+ stream-shift@1.0.3: {}
+
+ string-width@7.2.0:
+ dependencies:
+ emoji-regex: 10.5.0
+ get-east-asian-width: 1.4.0
+ strip-ansi: 7.1.2
+
+ string_decoder@1.1.1:
+ dependencies:
+ safe-buffer: 5.1.2
+
+ string_decoder@1.3.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ strip-ansi@7.1.2:
+ dependencies:
+ ansi-regex: 6.2.2
+
+ strip-json-comments@2.0.1: {}
+
+ strip-json-comments@3.1.1: {}
+
+ supports-color@5.5.0:
+ dependencies:
+ has-flag: 3.0.0
+
+ thread-stream@0.15.2:
+ dependencies:
+ real-require: 0.1.0
+
+ through2@2.0.5:
+ dependencies:
+ readable-stream: 2.3.8
+ xtend: 4.0.2
+
+ tinybench@2.9.0: {}
+
+ tinyexec@1.1.1: {}
+
+ tinyglobby@0.2.15:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.3)
+ picomatch: 4.0.3
+
+ tinyglobby@0.2.16:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
+ tinyrainbow@3.1.0: {}
+
+ traverse@0.6.8: {}
+
+ tree-kill@1.2.2: {}
+
+ tsdown@0.21.9(publint@0.3.18)(typescript@5.9.3):
+ dependencies:
+ ansis: 4.2.0
+ cac: 7.0.0
+ defu: 6.1.7
+ empathic: 2.0.0
+ hookable: 6.1.1
+ import-without-cache: 0.3.3
+ obug: 2.1.1
+ picomatch: 4.0.4
+ rolldown: 1.0.0-rc.16
+ rolldown-plugin-dts: 0.23.2(rolldown@1.0.0-rc.16)(typescript@5.9.3)
+ semver: 7.7.4
+ tinyexec: 1.1.1
+ tinyglobby: 0.2.16
+ tree-kill: 1.2.2
+ unconfig-core: 7.5.0
+ unrun: 0.2.36
+ optionalDependencies:
+ publint: 0.3.18
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - '@ts-macro/tsc'
+ - '@typescript/native-preview'
+ - oxc-resolver
+ - synckit
+ - vue-tsc
+
+ tslib@2.8.1:
+ optional: true
+
+ typescript@5.9.3: {}
+
+ unconfig-core@7.5.0:
+ dependencies:
+ '@quansync/fs': 1.0.0
+ quansync: 1.0.0
+
+ undici-types@7.13.0: {}
+
+ undici-types@7.16.0: {}
+
+ unrun@0.2.36:
+ dependencies:
+ rolldown: 1.0.0-rc.16
+
+ until-async@3.0.2: {}
+
+ util-deprecate@1.0.2: {}
+
+ vite@8.0.8(@types/node@24.6.2)(jiti@2.5.1):
+ dependencies:
+ lightningcss: 1.32.0
+ picomatch: 4.0.4
+ postcss: 8.5.10
+ rolldown: 1.0.0-rc.15
+ tinyglobby: 0.2.15
+ optionalDependencies:
+ '@types/node': 24.6.2
+ fsevents: 2.3.3
+ jiti: 2.5.1
+
+ vitest@4.1.4(@types/node@24.6.2)(vite@8.0.8(@types/node@24.6.2)(jiti@2.5.1)):
+ dependencies:
+ '@vitest/expect': 4.1.4
+ '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@24.6.2)(jiti@2.5.1))
+ '@vitest/pretty-format': 4.1.4
+ '@vitest/runner': 4.1.4
+ '@vitest/snapshot': 4.1.4
+ '@vitest/spy': 4.1.4
+ '@vitest/utils': 4.1.4
+ es-module-lexer: 2.0.0
+ expect-type: 1.3.0
+ magic-string: 0.30.21
+ obug: 2.1.1
+ pathe: 2.0.3
+ picomatch: 4.0.3
+ std-env: 4.1.0
+ tinybench: 2.9.0
+ tinyexec: 1.1.1
+ tinyglobby: 0.2.15
+ tinyrainbow: 3.1.0
+ vite: 8.0.8(@types/node@24.6.2)(jiti@2.5.1)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@types/node': 24.6.2
+ transitivePeerDependencies:
+ - msw
+
+ why-is-node-running@2.3.0:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
+ wrap-ansi@9.0.2:
+ dependencies:
+ ansi-styles: 6.2.3
+ string-width: 7.2.0
+ strip-ansi: 7.1.2
+
+ wrappy@1.0.2: {}
+
+ xtend@4.0.2: {}
+
+ y18n@5.0.8: {}
+
+ yargs-parser@22.0.0: {}
+
+ yargs@18.0.0:
+ dependencies:
+ cliui: 9.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ string-width: 7.2.0
+ y18n: 5.0.8
+ yargs-parser: 22.0.0
+
+ zod@4.3.6: {}
diff --git a/release.config.json b/release.config.json
new file mode 100644
index 00000000..3aae004e
--- /dev/null
+++ b/release.config.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "./node_modules/@ossjs/release/schema.json",
+ "profiles": [
+ {
+ "name": "latest",
+ "use": "NPM_CONFIG_PROVENANCE=true pnpm publish --no-git-checks --access=public"
+ }
+ ]
+}
diff --git a/simple-git-hooks.js b/simple-git-hooks.js
deleted file mode 100644
index e3a56919..00000000
--- a/simple-git-hooks.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = {
- 'prepare-commit-msg': `grep -qE '^[^#]' .git/COMMIT_EDITMSG || (exec < /dev/tty && yarn cz --hook || true)`,
- 'commit-msg': 'yarn commitlint --edit $1',
-}
diff --git a/src/collection.ts b/src/collection.ts
new file mode 100644
index 00000000..83ed7898
--- /dev/null
+++ b/src/collection.ts
@@ -0,0 +1,995 @@
+import type { StandardSchemaV1 } from '@standard-schema/spec'
+import { get } from 'es-toolkit/compat'
+import { apply, create as createDraft, type Draft, type Patch } from 'mutative'
+import { invariant, InvariantError } from 'outvariant'
+import { Logger } from '#/src/logger.js'
+import { createHooksEmitter, type HookEventMap } from '#/src/hooks.js'
+import { Query } from '#/src/query.js'
+import {
+ createRelationBuilder,
+ Relation,
+ type RelationsFunction,
+} from '#/src/relation.js'
+import {
+ cloneWithInternals,
+ definePropertyAtPath,
+ isObject,
+ isRecord,
+ toDeepEntries,
+} from '#/src/utils.js'
+import { type SortOptions, sortResults } from '#/src/sort.js'
+import type { Extension } from '#/src/extensions/index.js'
+import { OperationError, OperationErrorCodes } from '#/src/errors.js'
+import { TypedEvent, type Emitter } from 'rettime'
+
+let collectionsCreated = 0
+
+export type CollectionOptions = {
+ /**
+ * A [Standard Schema](https://standardschema.dev/) describing the records in this collection.
+ */
+ schema: Schema
+ /**
+ * Extensions to apply to this collection.
+ */
+ extensions?: Array
+}
+
+export interface PaginationOptions {
+ /**
+ * A reference to a record to use as a cursor to start the querying from.
+ */
+ cursor?: RecordType>
+ /**
+ * A number of matching records to take (after `skip`, if any).
+ */
+ take?: number
+ /**
+ * A number of matching records to skip.
+ */
+ skip?: number
+}
+
+export interface UpdateOptions {
+ data: UpdateFunction
+}
+
+interface StrictOptions {
+ /**
+ * Throws an error if no records match the given query.
+ */
+ strict?: Strict
+}
+
+export type UpdateFunction = (draft: Draft) => Promise | void
+
+export type RecordType> = V & {
+ [kPrimaryKey]: string
+ [kRelationMap]: Map
+}
+
+export const kCollectionId = Symbol('kCollectionId')
+export const kPrimaryKey = Symbol('kPrimaryKey')
+export const kRelationMap = Symbol('kRelationMap')
+
+/**
+ * A collection of data.
+ * @example
+ * const users = new Collection({ schema: userSchema })
+ */
+export class Collection {
+ #records: Array>>
+ #logger: Logger
+
+ private [kCollectionId]: number
+
+ public hooks: Emitter>
+
+ constructor(private readonly options: CollectionOptions) {
+ this[kCollectionId] = this.#generateCollectionId()
+
+ this.#logger = new Logger('Collection').extend(this[kCollectionId])
+ this.#records = []
+
+ this.hooks = createHooksEmitter()
+ this.options.extensions?.forEach((extension) => extension.extend(this))
+ }
+
+ /**
+ * Creates a new record with the given values.
+ * @param initialValues Initial values for the new record.
+ * @return The created record.
+ *
+ * @example
+ * await users.create({ id: 1, name: 'John' })
+ */
+ public async create(
+ initialValues: StandardSchemaV1.InferInput,
+ ): Promise>> {
+ let logger = this.#logger.extend('create')
+ logger.log('initial values:', initialValues)
+
+ const { sanitizedInitialValues, restoreProperties } =
+ this.#sanitizeInitialValues(initialValues)
+
+ const validationResult = await this.options.schema['~standard'].validate(
+ sanitizedInitialValues,
+ )
+
+ if (validationResult.issues) {
+ console.error(validationResult.issues)
+
+ throw new OperationError(
+ 'Failed to create a new record with initial values: does not match the schema. Please see the schema validation errors above.',
+ OperationErrorCodes.INVALID_INITIAL_VALUES,
+ )
+ }
+
+ let record = validationResult.value as RecordType
+
+ invariant.as(
+ OperationError.for(OperationErrorCodes.INVALID_INITIAL_VALUES),
+ typeof record === 'object',
+ 'Failed to create a record with initial values (%j): expected the record to be an object or an array',
+ initialValues,
+ )
+
+ restoreProperties(record)
+
+ // Generate random primary key for every record.
+ const primaryKey =
+ (isObject(initialValues) &&
+ initialValues[kPrimaryKey as keyof typeof initialValues]) ||
+ crypto.randomUUID()
+
+ Object.defineProperties(record, {
+ [kPrimaryKey]: {
+ enumerable: false,
+ configurable: false,
+ value: primaryKey,
+ },
+ [kRelationMap]: {
+ enumerable: false,
+ configurable: false,
+ value: new Map>(),
+ },
+ })
+
+ logger = logger.extend(primaryKey)
+ logger.log('symbols defined!', record[kRelationMap])
+
+ if (this.hooks.listenerCount('create') > 0) {
+ await this.hooks.emitAsPromise(
+ new TypedEvent('create', { data: { record, initialValues } }),
+ )
+ }
+ logger.log('create hooks done!')
+
+ this.#records.push(record)
+ logger.log('create done!', record)
+
+ return record
+ }
+
+ /**
+ * Creates multiple records using the given initial values factory.
+ * @param count Number of records to create.
+ * @param initialValuesFactory Factory function to generate initial values for each record.
+ * @return Array of created records.
+ *
+ * @example
+ * await users.createMany(5, (index) => ({ id: index + 1}))
+ */
+ public async createMany(
+ count: number,
+ initialValuesFactory: (
+ index: number,
+ ) => StandardSchemaV1.InferInput,
+ ): Promise>>> {
+ const pendingPromises: Array> = []
+
+ for (let i = 0; i < count; i++) {
+ pendingPromises.push(this.create(initialValuesFactory(i)))
+ }
+
+ return await Promise.all(pendingPromises).catch((error) => {
+ throw new OperationError(
+ 'Failed to execute "createMany" on collection: unexpected error',
+ OperationErrorCodes.UNEXPECTED_ERROR,
+ error,
+ )
+ })
+ }
+
+ /**
+ * Returns the first record matching the query.
+ * If no query is provided, returns the first record in the collection.
+ * @example
+ * users.findFirst((q) => q.where({ id: 123 }))
+ */
+ public findFirst(
+ predicate?:
+ | ((query: Query>>) => Query)
+ | Query>>,
+ options?: StrictOptions,
+ ): Strict extends true
+ ? RecordType>
+ : RecordType> | undefined {
+ if (predicate == null) {
+ const firstRecord = this.#records[0]
+
+ invariant.as(
+ OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
+ options?.strict ? firstRecord != null : true,
+ 'Failed to execute "findFirst" on collection without a query: the collection is empty',
+ )
+
+ return firstRecord!
+ }
+
+ const result = this.#query(
+ predicate instanceof Query ? predicate : predicate(new Query()),
+ ).next().value
+
+ invariant.as(
+ OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
+ options?.strict ? result != null : true,
+ 'Failed to execute "findFirst" on collection: no record found matching the query',
+ )
+
+ return result!
+ }
+
+ /**
+ * Returns all records matching the query.
+ * If no query is provided, returns all records in the collection.
+ * @example
+ * users.findMany((q) => q.where({ subscribed: false }))
+ */
+ public findMany(
+ predicate?:
+ | ((query: Query>>) => Query)
+ | Query>>,
+ options?: PaginationOptions & SortOptions & StrictOptions,
+ ): Array>> {
+ const query =
+ predicate == null
+ ? new Query(() => true)
+ : predicate instanceof Query
+ ? predicate
+ : predicate(new Query())
+
+ const results = Array.from(this.#query(query, options)).filter(
+ (result) => !!result,
+ )
+
+ invariant.as(
+ OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
+ options?.strict ? results.length > 0 : true,
+ 'Failed to execute "findMany" on collection: no records found matching the query',
+ )
+
+ if (options?.orderBy) {
+ sortResults(options, results)
+ }
+
+ return results
+ }
+
+ /**
+ * Updates the first record matching the query.
+ * Returns the updated record.
+ * @example
+ * await users.update(
+ * (q) => q.where({ name: 'John' }),
+ * {
+ * data(user) {
+ * user.name = 'Johnatan'
+ * }
+ * }
+ * )
+ */
+ public async update(
+ predicate:
+ | ((query: Query>>) => Query)
+ | Query>>
+ | RecordType>,
+ options: UpdateOptions> &
+ StrictOptions,
+ ): Promise<
+ Strict extends true
+ ? RecordType>
+ : RecordType> | undefined
+ > {
+ const prevRecord = this.findFirst(
+ isRecord(predicate)
+ ? new Query((record) => {
+ return record[kPrimaryKey] === predicate[kPrimaryKey]
+ })
+ : predicate,
+ )
+
+ if (prevRecord == null) {
+ invariant.as(
+ OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
+ !options.strict,
+ 'Failed to execute "update" on collection: no record found matching the query',
+ )
+
+ return undefined!
+ }
+
+ const nextRecord = await this.#produceRecord(prevRecord, options.data)
+ this.#replaceRecord(prevRecord, nextRecord)
+
+ return nextRecord
+ }
+
+ /**
+ * Updates all records matching the query.
+ * Resolves to the list of updated records.
+ * @example
+ * await users.updateMany(
+ * (q) => q.where({ subscribed: false }),
+ * {
+ * data(user) {
+ * user.subscribed = true
+ * }
+ * }
+ * )
+ */
+ public async updateMany(
+ predicate:
+ | ((query: Query>>) => Query)
+ | Query>>,
+ options: UpdateOptions> &
+ SortOptions &
+ StrictOptions,
+ ): Promise>>> {
+ const prevRecords = this.findMany(predicate)
+
+ if (prevRecords.length === 0) {
+ invariant.as(
+ OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
+ !options.strict,
+ 'Failed to execute "updateMany" on collection: no records found matching the query',
+ )
+
+ return []
+ }
+
+ const nextRecords = []
+
+ for (const prevRecord of prevRecords) {
+ const nextRecord = await this.#produceRecord(prevRecord, options.data)
+ this.#replaceRecord(prevRecord, nextRecord)
+ nextRecords.push(nextRecord)
+ }
+
+ if (options.orderBy) {
+ sortResults(options, nextRecords)
+ }
+
+ return nextRecords
+ }
+
+ /**
+ * Deletes the first record matching the query.
+ * @example
+ * users.delete((q) => q.where({ id: 123 }))
+ */
+ public delete(
+ predicate:
+ | ((query: Query>>) => Query)
+ | Query>>
+ | RecordType>,
+ options?: StrictOptions,
+ ): Strict extends true
+ ? RecordType>
+ : RecordType> | undefined {
+ if (isRecord(predicate)) {
+ this.#deleteRecord(predicate)
+ return predicate
+ }
+
+ const record = this.findFirst(predicate)
+
+ if (record == null) {
+ invariant.as(
+ OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
+ !options?.strict,
+ 'Failed to execute "delete" on collection: no record found matching the query',
+ )
+
+ return undefined!
+ }
+
+ this.#deleteRecord(record)
+ return record
+ }
+
+ /**
+ * Deletes all records matching the query.
+ * @example
+ * users.deleteMany((q) => q.where({ subscribed: false }))
+ */
+ public deleteMany(
+ predicate:
+ | ((query: Query>>) => Query)
+ | Query>>,
+ options?: SortOptions & StrictOptions,
+ ): Array>> {
+ /**
+ * @note Do NOT forward the sorting options to the querying phase
+ * so the results are returned in the order they are present in the store.
+ * That way, we can delete them right-to-left correctly.
+ */
+ const records = this.findMany(predicate)
+
+ for (let i = records.length - 1; i >= 0; i--) {
+ this.#deleteRecord(records[i]!)
+ }
+
+ if (records.length === 0) {
+ invariant.as(
+ OperationError.for(OperationErrorCodes.STRICT_QUERY_WITHOUT_RESULTS),
+ !options?.strict,
+ 'Failed to execute "deleteMany" on collection: no records found matching the query',
+ )
+
+ return []
+ }
+
+ if (options?.orderBy) {
+ sortResults(options, records)
+ }
+
+ return records
+ }
+
+ /**
+ * Returns the total number of records in this collection.
+ * @example
+ * const users = new Collection({ schema })
+ * await users.create({ id: 1, name: 'John' })
+ * users.count() // 1
+ */
+ public count(): number {
+ return this.#records.length
+ }
+
+ /**
+ * Returns a list of all records from this collection.
+ */
+ public all(): Array>> {
+ /**
+ * @note Preserve exact record references so they might be used
+ * when querying (must contain primary keys).
+ */
+ return this.#records
+ }
+
+ /**
+ * Deletes all the records in this collection.
+ */
+ public clear(): void {
+ for (const record of this.#records) {
+ this.#deleteRecord(record)
+ }
+
+ this.#records.length = 0
+ }
+
+ /**
+ * Defines relations for the records in this collection.
+ * @example
+ * users.defineRelations(({ many }) => ({
+ * posts: many(posts),
+ * }))
+ */
+ public defineRelations(
+ resolver: RelationsFunction>,
+ ) {
+ let logger = this.#logger.extend('defineRelations')
+ logger.log('defining relations...')
+
+ const relations = toDeepEntries<() => Relation>(
+ resolver(createRelationBuilder(this)) as any,
+ )
+ logger.log('relations declaration:', relations)
+
+ const initializeRelations = (
+ record: RecordType,
+ initialValues: StandardSchemaV1.InferInput = record,
+ ) => {
+ for (const [path, createRelation] of relations) {
+ logger.log(`initializing relation for "${path.join('.')}"...`)
+
+ const relation = createRelation()
+ relation.initialize(record, path as Array, initialValues)
+
+ logger.log('relation initialized!', relation)
+ }
+ }
+
+ // Initialize relations for the existing records that were created
+ // before these relations were defined.
+ for (const record of this.#records) {
+ initializeRelations(record)
+ }
+
+ // Initialize relations for all records created from now on.
+ this.hooks.earlyOn('create', (event) => {
+ initializeRelations(event.data.record, event.data.initialValues)
+ })
+ }
+
+ /**
+ * Sanitizes the given object so it can be accepted as the input to Standard Schema validation.
+ * This removes getters to prevent potentially infinite object references in self-referencing
+ * relations. This also drops the internal symbols but gives a function to restore them back.
+ */
+ #sanitizeInitialValues(initialValues: unknown) {
+ const propertiesToRestore: Array<{
+ path: Array
+ descriptor: PropertyDescriptor
+ }> = []
+
+ // Track visited records by primary key to detect cycles
+ // in self-referencing relations. Only strip relation values
+ // when revisiting a record (i.e. an actual cycle), not for
+ // all nested records indiscriminately.
+ const visited = new Set()
+
+ const sanitize = (
+ value: unknown,
+ path: Array = [],
+ ): unknown => {
+ if (Array.isArray(value)) {
+ return value.map((value, index) => sanitize(value, path.concat(index)))
+ }
+
+ if (isObject(value)) {
+ const record = isRecord(value) ? value : undefined
+ const isRevisit = record != null && visited.has(record[kPrimaryKey])
+
+ if (record && !isRevisit) {
+ visited.add(record[kPrimaryKey])
+ }
+
+ const relations = record ? record[kRelationMap] : undefined
+
+ return Object.fromEntries(
+ Reflect.ownKeys(value).map((key) => {
+ const childValue = value[key as keyof typeof value]
+ const childPath = path.concat(key)
+
+ if (typeof key === 'symbol') {
+ /**
+ * @note Preserve primary keys on sanitized initial values.
+ * Otherwise, internal symbols are stripped off and record references are lost.
+ * This is curcial when handling relations for records that were created
+ * before the relation was defined.
+ */
+ if (key === kPrimaryKey) {
+ propertiesToRestore.push({
+ path: childPath,
+ descriptor: Object.getOwnPropertyDescriptor(value, key)!,
+ })
+ }
+ return [key, childValue]
+ }
+
+ const relation = relations?.get(key)
+
+ // Only strip relation values when revisiting a record
+ // to break self-referencing cycles. Non-circular nested
+ // relations are left intact for proper schema validation.
+ if (isRevisit && relation && childValue != null) {
+ propertiesToRestore.push({
+ path: childPath,
+ descriptor: Object.getOwnPropertyDescriptor(value, key)!,
+ })
+ return [key, relation.getDefaultValue()]
+ }
+
+ return [key, sanitize(childValue, childPath)]
+ }),
+ )
+ }
+
+ return value
+ }
+
+ const sanitizedInitialValues = sanitize(initialValues)
+
+ return {
+ sanitizedInitialValues,
+ /**
+ * Restores record properties that were stripped off during the sanitization
+ * (e.g. relational properties, internal symbols of records given as initial value, etc).
+ */
+ restoreProperties(record: RecordType): void {
+ for (const { path, descriptor } of propertiesToRestore) {
+ definePropertyAtPath(record, path, descriptor)
+ }
+ },
+ }
+ }
+
+ *#query(
+ query: Query>>,
+ options: PaginationOptions = { take: Infinity },
+ ): Generator<
+ RecordType> | undefined,
+ undefined,
+ RecordType> | undefined
+ > {
+ const { take, cursor, skip } = options
+
+ invariant(
+ skip !== undefined ? Number.isInteger(skip) && skip >= 0 : true,
+ 'Failed to query the collection: expected the "skip" pagination option to be a number larger or equal to 0 but got %j',
+ skip,
+ )
+
+ let taken = 0
+ let skipped = 0
+
+ // if (cursor != null) {
+ // const cursorIndex = store.findIndex((record) => {
+ // return record[kPrimaryKey] === cursor[kPrimaryKey]
+ // })
+
+ // if (cursorIndex === -1) {
+ // return
+ // }
+
+ // store = store.slice(cursorIndex + 1)
+ // }
+
+ const shouldTake = Math.abs(take ?? Infinity)
+ const delta = take && take < 0 ? -1 : 1
+ let start = delta === 1 ? 0 : this.#records.length - 1
+ const end = delta === 1 ? this.#records.length : -1
+
+ if (cursor != null) {
+ const cursorIndex = this.#records.findIndex((record) => {
+ return record[kPrimaryKey] === cursor[kPrimaryKey]
+ })
+
+ if (cursorIndex === -1) {
+ return
+ }
+
+ start = cursorIndex
+ }
+
+ for (let i = start; i !== end; i += delta) {
+ const record = this.#records[i]
+
+ if (record != null && query.test(record)) {
+ if (skip != null) {
+ if (skipped < skip) {
+ skipped++
+ continue
+ }
+ }
+
+ yield record
+ taken++
+ }
+
+ if (taken >= shouldTake) {
+ break
+ }
+ }
+ }
+
+ /**
+ * Returns the index of the given record in this collection.
+ * Performs a primary key-based lookup instead of a reference lookup
+ * because certain references (like root-level arrays) might become stale
+ * after updates, but will retain their primary keys.
+ */
+ #indexOf(record: RecordType): number {
+ return this.#records.findIndex((existingRecord) => {
+ return existingRecord[kPrimaryKey] === record[kPrimaryKey]
+ })
+ }
+
+ /**
+ * Replaces the given record with the next version of it.
+ */
+ #replaceRecord(prevRecord: RecordType, nextRecord: RecordType): void {
+ const index = this.#indexOf(prevRecord)
+
+ invariant(
+ index !== -1,
+ 'Failed to replace record "%j" with "%j": previous record not found',
+ prevRecord,
+ nextRecord,
+ )
+
+ this.#records[index] = nextRecord
+ }
+
+ /**
+ * Deletes the given record from the collection.
+ */
+ #deleteRecord(record: RecordType): void {
+ const index = this.#indexOf(record)
+
+ if (index !== -1) {
+ const deleteEvent = new TypedEvent('delete', {
+ data: { deletedRecord: record },
+ })
+ this.hooks.emit(deleteEvent)
+
+ if (!deleteEvent.defaultPrevented) {
+ this.#records.splice(index, 1)
+ }
+ }
+ }
+
+ /**
+ * Produces the next version of the given record by applying the `data` changes to it.
+ * Re-applies the schema to the end record to ensure validity and apply user-defined transforms.
+ */
+ async #produceRecord(
+ prevRecord: RecordType>,
+ updateData: UpdateOptions>['data'],
+ ): Promise>> {
+ const logger = this.#logger.extend('produceRecord')
+ logger.log('updating the record with options:', prevRecord, updateData)
+
+ /**
+ * @note Clone the previous record, preserving the symbols (so it's considered a record)
+ * but stripping off relational keys (getters) to preserve the values of foreign records
+ * at the moment of update.
+ */
+ const frozenPrevRecord = cloneWithInternals(
+ prevRecord,
+ ({ key, descriptor }) => {
+ return typeof key === 'symbol' && descriptor.get == null
+ },
+ )
+
+ invariant(
+ isRecord(frozenPrevRecord),
+ 'Failed to update a record (%j): frozen previous record copy is not a record',
+ prevRecord,
+ )
+
+ /**
+ * @note Build a draft input where relational getters are replaced with
+ * plain data descriptors holding their resolved values. `mutative` cannot
+ * observe mutations through getters (https://github.com/unadlib/mutative/issues/157),
+ * so a `push` on `draft.posts` is lost unless `posts` is a plain property.
+ * Live foreign record references (with their internal symbols) are preserved.
+ */
+ const draftInput = this.#buildDraftInput(prevRecord)
+
+ const [maybeNextRecord, patches, inversePatches] = await createDraft(
+ draftInput,
+ updateData,
+ {
+ strict: false,
+ enablePatches: true,
+ },
+ )
+
+ Object.defineProperties(maybeNextRecord, {
+ [kPrimaryKey]: {
+ value: prevRecord[kPrimaryKey],
+ enumerable: false,
+ configurable: false,
+ },
+ [kRelationMap]: {
+ value: prevRecord[kRelationMap],
+ enumerable: false,
+ configurable: false,
+ },
+ })
+
+ invariant(
+ isRecord(maybeNextRecord),
+ 'Failed to update a record (%j): a record produced by the draft is not a record',
+ prevRecord,
+ )
+
+ // Route the updates produces by the draft through the hooks
+ // so the hooks could reverse some of them.
+ const patchesToUndo: Array = []
+
+ /**
+ * @note Collapse per-index patches under a relation path into a single
+ * relation-level event. `draft.posts.push(x)` emits `{path: ['posts', N]}`,
+ * but relation handlers expect `{path: ['posts'], nextValue: }`.
+ */
+ const relationPaths: Array> = []
+ for (const serializedPath of prevRecord[kRelationMap].keys()) {
+ relationPaths.push(serializedPath.split('.'))
+ }
+
+ type RelationPatchGroup = {
+ relationPath: Array
+ patchIndices: Array
+ }
+ const relationGroups = new Map()
+ const passthroughIndices: Array = []
+
+ for (let i = 0; i < patches.length; i++) {
+ const patch = patches[i]
+ if (!patch) {
+ continue
+ }
+
+ const matchingRelationPath = relationPaths.find((relationPath) => {
+ if (patch.path.length !== relationPath.length + 1) {
+ return false
+ }
+ if (!relationPath.every((key, index) => key === patch.path[index])) {
+ return false
+ }
+ const nextSegment = patch.path[relationPath.length]
+ return typeof nextSegment === 'number' || nextSegment === 'length'
+ })
+
+ if (matchingRelationPath) {
+ const groupKey = matchingRelationPath.join('.')
+ const group = relationGroups.get(groupKey) ?? {
+ relationPath: matchingRelationPath,
+ patchIndices: [],
+ }
+ group.patchIndices.push(i)
+ relationGroups.set(groupKey, group)
+ } else {
+ passthroughIndices.push(i)
+ }
+ }
+
+ for (const i of passthroughIndices) {
+ const patch = patches[i]!
+
+ const updateEvent = new TypedEvent('update', {
+ data: {
+ prevRecord: frozenPrevRecord,
+ nextRecord: maybeNextRecord,
+ path: patch.path,
+ prevValue: get(prevRecord, patch.path),
+ nextValue: patch.value,
+ },
+ })
+
+ this.hooks.emit(updateEvent)
+
+ if (updateEvent.defaultPrevented) {
+ const inversePatch = inversePatches[i]
+
+ invariant(
+ inversePatch != null,
+ 'Failed to update a record (%j): missing inverse patch at index %d',
+ prevRecord,
+ i,
+ )
+
+ patchesToUndo.push(inversePatch)
+ }
+ }
+
+ for (const group of relationGroups.values()) {
+ const updateEvent = new TypedEvent('update', {
+ data: {
+ prevRecord: frozenPrevRecord,
+ nextRecord: maybeNextRecord,
+ path: group.relationPath,
+ prevValue: get(prevRecord, group.relationPath),
+ nextValue: get(maybeNextRecord, group.relationPath),
+ },
+ })
+
+ this.hooks.emit(updateEvent)
+
+ if (updateEvent.defaultPrevented) {
+ for (const i of group.patchIndices) {
+ const inversePatch = inversePatches[i]
+
+ invariant(
+ inversePatch != null,
+ 'Failed to update a record (%j): missing inverse patch at index %d',
+ prevRecord,
+ i,
+ )
+
+ patchesToUndo.push(inversePatch)
+ }
+ }
+ }
+
+ const nextRecord =
+ patchesToUndo.length > 0
+ ? apply(maybeNextRecord, patchesToUndo)
+ : maybeNextRecord
+
+ logger.log('re-applying the schema...')
+ const { sanitizedInitialValues } = this.#sanitizeInitialValues(nextRecord)
+ const validationResult = await this.options.schema['~standard'].validate(
+ sanitizedInitialValues,
+ )
+
+ if (validationResult.issues) {
+ console.error(validationResult.issues)
+ throw new InvariantError(
+ 'Failed to update record (%j): resulting record does not match the schema',
+ frozenPrevRecord,
+ )
+ }
+
+ const finalRecord = validationResult.value as RecordType
+ logger.log('schema re-applied!')
+
+ const descriptors = Object.getOwnPropertyDescriptors(prevRecord)
+ for (const key of Reflect.ownKeys(descriptors)) {
+ const descriptor = descriptors[key as keyof typeof descriptors]
+ if (typeof key === 'symbol' || typeof descriptor.get === 'function') {
+ Object.defineProperty(finalRecord, key, descriptor)
+ }
+ }
+
+ return finalRecord
+ }
+
+ /**
+ * Build a draftable copy of the record where relational getters are replaced
+ * with data descriptors holding their resolved values. `mutative` cannot
+ * observe mutations through getters (https://github.com/unadlib/mutative/issues/157),
+ * so a `push` on `draft.posts` is lost unless `posts` is a plain property.
+ * Live foreign record references (with their internal symbols) are preserved
+ * so downstream hooks can resolve primary keys on the drafted elements.
+ */
+ #buildDraftInput(record: T): T {
+ const clone = cloneWithInternals(
+ record,
+ ({ key, descriptor }) =>
+ typeof key === 'symbol' && descriptor.get == null,
+ )
+
+ for (const serializedPath of record[kRelationMap].keys()) {
+ const path = serializedPath.split('.')
+ definePropertyAtPath(clone, path, {
+ value: get(record, path),
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ })
+ }
+
+ return clone
+ }
+
+ /**
+ * Returns a reproducible collection ID number based on the collection
+ * creation order. Collection ID has to be reproducible across runtimes
+ * to enable synchronization.
+ */
+ #generateCollectionId(): number {
+ collectionsCreated++
+ const seed = 0
+ const value = collectionsCreated.toString()
+
+ let h1 = 0xdeadbeef ^ seed,
+ h2 = 0x41c6ce57 ^ seed
+ for (let i = 0, ch; i < value.length; i++) {
+ ch = value.charCodeAt(i)
+ h1 = Math.imul(h1 ^ ch, 2654435761)
+ h2 = Math.imul(h2 ^ ch, 1597334677)
+ }
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507)
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909)
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507)
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909)
+
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0)
+ }
+}
diff --git a/src/comparators/boolean.ts b/src/comparators/boolean.ts
deleted file mode 100644
index e1afb636..00000000
--- a/src/comparators/boolean.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { BooleanQuery, QueryToComparator } from '../query/queryTypes'
-
-export const booleanComparators: QueryToComparator = {
- equals(expected, actual) {
- return actual === expected
- },
- notEquals(expected, actual) {
- return expected !== actual
- },
-}
diff --git a/src/comparators/date.ts b/src/comparators/date.ts
deleted file mode 100644
index bdebe2ae..00000000
--- a/src/comparators/date.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { compareAsc as compareDates } from 'date-fns'
-import { DateQuery, QueryToComparator } from '../query/queryTypes'
-
-export const dateComparators: QueryToComparator = {
- equals(expected, actual) {
- return compareDates(expected, actual) === 0
- },
- notEquals(expected, actual) {
- return compareDates(expected, actual) !== 0
- },
- gt(expected, actual) {
- return compareDates(actual, expected) === 1
- },
- gte(expected, actual) {
- return [0, 1].includes(compareDates(actual, expected))
- },
- lt(expected, actual) {
- return compareDates(actual, expected) === -1
- },
- lte(expected, actual) {
- return [-1, 0].includes(compareDates(actual, expected))
- },
-}
diff --git a/src/comparators/number.ts b/src/comparators/number.ts
deleted file mode 100644
index f56a0594..00000000
--- a/src/comparators/number.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { NumberQuery, QueryToComparator } from '../query/queryTypes'
-import { numberInRange } from '../utils/numberInRange'
-
-export const numberComparators: QueryToComparator = {
- equals(expected, actual) {
- return actual === expected
- },
- notEquals(expected, actual) {
- return !numberComparators.equals(expected, actual)
- },
- between(expected, actual) {
- return numberInRange(expected[0], expected[1], actual)
- },
- notBetween(expected, actual) {
- return !numberComparators.between(expected, actual)
- },
- gt(expected, actual) {
- return actual > expected
- },
- gte(expected, actual) {
- return actual >= expected
- },
- lt(expected, actual) {
- return actual < expected
- },
- lte(expected, actual) {
- return actual <= expected
- },
- in(expected, actual) {
- return expected.includes(actual)
- },
- notIn(expected, actual) {
- return !numberComparators.in(expected, actual)
- },
-}
diff --git a/src/comparators/string.ts b/src/comparators/string.ts
deleted file mode 100644
index a44d2ac5..00000000
--- a/src/comparators/string.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { QueryToComparator, StringQuery } from '../query/queryTypes'
-
-export const stringComparators: QueryToComparator = {
- equals(expected, actual) {
- return expected === actual
- },
- notEquals(expected, actual) {
- return !stringComparators.equals(expected, actual)
- },
- contains(expected, actual) {
- return actual.includes(expected)
- },
- notContains(expected, actual) {
- return !stringComparators.contains(expected, actual)
- },
- in(expected, actual) {
- return expected.includes(actual)
- },
- notIn(expected, actual) {
- return !stringComparators.in(expected, actual)
- },
-}
diff --git a/src/db/Database.ts b/src/db/Database.ts
deleted file mode 100644
index 92745e13..00000000
--- a/src/db/Database.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import md5 from 'md5'
-import { invariant } from 'outvariant'
-import { StrictEventEmitter } from 'strict-event-emitter'
-import {
- Entity,
- ENTITY_TYPE,
- KeyType,
- ModelDictionary,
- PrimaryKeyType,
- PRIMARY_KEY,
-} from '../glossary'
-
-export const SERIALIZED_INTERNAL_PROPERTIES_KEY =
- 'SERIALIZED_INTERNAL_PROPERTIES'
-
-type Models = Record<
- keyof Dictionary,
- Map>
->
-
-export interface SerializedInternalEntityProperties {
- entityType: string
- primaryKey: PrimaryKeyType
-}
-
-export interface SerializedEntity extends Entity {
- [SERIALIZED_INTERNAL_PROPERTIES_KEY]: SerializedInternalEntityProperties
-}
-
-export type DatabaseMethodToEventFn = (
- sourceId: string,
- args: ArgsType,
-) => void
-
-export interface DatabaseEventsMap {
- create: DatabaseMethodToEventFn<
- [
- modelName: KeyType,
- entity: SerializedEntity,
- customPrimaryKey?: PrimaryKeyType,
- ]
- >
- update: DatabaseMethodToEventFn<
- [
- modelName: KeyType,
- prevEntity: SerializedEntity,
- nextEntity: SerializedEntity,
- ]
- >
- delete: DatabaseMethodToEventFn<
- [modelName: KeyType, primaryKey: PrimaryKeyType]
- >
-}
-
-let callOrder = 0
-
-export class Database {
- public id: string
- public events: StrictEventEmitter
- private models: Models
-
- constructor(dictionary: Dictionary) {
- this.events = new StrictEventEmitter()
- this.models = Object.keys(dictionary).reduce>(
- (acc, modelName: keyof Dictionary) => {
- acc[modelName] = new Map>()
- return acc
- },
- {} as Models,
- )
-
- callOrder++
- this.id = this.generateId()
- }
-
- /**
- * Generates a unique MD5 hash based on the database
- * module location and invocation order. Used to reproducibly
- * identify a database instance among sibling instances.
- */
- private generateId() {
- const { stack } = new Error()
- const callFrame = stack?.split('\n')[4]
- const salt = `${callOrder}-${callFrame?.trim()}`
- return md5(salt)
- }
-
- private serializeEntity(entity: Entity): SerializedEntity {
- return {
- ...entity,
- [SERIALIZED_INTERNAL_PROPERTIES_KEY]: {
- entityType: entity[ENTITY_TYPE],
- primaryKey: entity[PRIMARY_KEY],
- },
- }
- }
-
- getModel(name: ModelName) {
- return this.models[name]
- }
-
- create(
- modelName: ModelName,
- entity: Entity,
- customPrimaryKey?: PrimaryKeyType,
- ): Map> {
- invariant(
- entity[ENTITY_TYPE],
- 'Failed to create a new "%s" record: provided entity has no type. %j',
- modelName,
- entity,
- )
- invariant(
- entity[PRIMARY_KEY],
- 'Failed to create a new "%s" record: provided entity has no primary key. %j',
- modelName,
- entity,
- )
-
- const primaryKey =
- customPrimaryKey || (entity[entity[PRIMARY_KEY]] as string)
-
- this.events.emit('create', this.id, [
- modelName,
- this.serializeEntity(entity),
- customPrimaryKey,
- ])
- return this.getModel(modelName).set(primaryKey, entity)
- }
-
- update(
- modelName: ModelName,
- prevEntity: Entity,
- nextEntity: Entity,
- ): void {
- const prevPrimaryKey = prevEntity[prevEntity[PRIMARY_KEY]] as PrimaryKeyType
- const nextPrimaryKey = nextEntity[prevEntity[PRIMARY_KEY]] as PrimaryKeyType
-
- if (nextPrimaryKey !== prevPrimaryKey) {
- this.delete(modelName, prevPrimaryKey)
- }
-
- this.getModel(modelName).set(nextPrimaryKey, nextEntity)
-
- // this.create(modelName, nextEntity, nextPrimaryKey)
- this.events.emit('update', this.id, [
- modelName,
- this.serializeEntity(prevEntity),
- this.serializeEntity(nextEntity),
- ])
- }
-
- delete(
- modelName: ModelName,
- primaryKey: PrimaryKeyType,
- ): void {
- this.getModel(modelName).delete(primaryKey)
- this.events.emit('delete', this.id, [modelName, primaryKey])
- }
-
- has(
- modelName: ModelName,
- primaryKey: PrimaryKeyType,
- ): boolean {
- return this.getModel(modelName).has(primaryKey)
- }
-
- count(modelName: ModelName) {
- return this.getModel(modelName).size
- }
-
- listEntities(
- modelName: ModelName,
- ): Entity[] {
- return Array.from(this.getModel(modelName).values())
- }
-}
diff --git a/src/db/drop.ts b/src/db/drop.ts
deleted file mode 100644
index 8f382b30..00000000
--- a/src/db/drop.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { FactoryAPI } from '../glossary'
-
-export function drop(factoryApi: FactoryAPI): void {
- Object.values(factoryApi).forEach((model) => {
- model.deleteMany({ where: {} })
- })
-}
diff --git a/src/errors.ts b/src/errors.ts
new file mode 100644
index 00000000..bce08cc4
--- /dev/null
+++ b/src/errors.ts
@@ -0,0 +1,56 @@
+import type { Collection } from '#/src/collection.js'
+import type { PropertyPath } from '#/src/utils.js'
+import type { RelationDeclarationOptions } from '#/src/relation.js'
+
+export enum OperationErrorCodes {
+ UNEXPECTED_ERROR = 'UNEXPECTED_ERROR',
+ INVALID_INITIAL_VALUES = 'INVALID_INITIAL_VALUES',
+ STRICT_QUERY_WITHOUT_RESULTS = 'STRICT_QUERY_WITHOUT_RESULTS',
+}
+
+export class OperationError extends Error {
+ static for(code: OperationErrorCodes) {
+ return (message: string) => {
+ return new OperationError(message, code)
+ }
+ }
+
+ constructor(
+ message: string,
+ public readonly code: OperationErrorCodes,
+ public readonly cause?: unknown,
+ ) {
+ super(message)
+ }
+}
+
+export enum RelationErrorCodes {
+ RELATION_NOT_READY = 'RELATION_NOT_READY',
+ UNEXPECTED_SET_EXPRESSION = 'UNEXPECTED_SET_EXPRESSION',
+ INVALID_FOREIGN_RECORD = 'INVALID_FOREIGN_RECORD',
+ FORBIDDEN_UNIQUE_CREATE = 'FORBIDDEN_UNIQUE_CREATE',
+ FORBIDDEN_UNIQUE_UPDATE = 'FORBIDDEN_UNIQUE_UPDATE',
+}
+
+export interface RelationErrorDetails {
+ path: PropertyPath
+ ownerCollection: Collection
+ foreignCollections: Array>
+ options: RelationDeclarationOptions
+}
+
+export class RelationError extends Error {
+ static for(code: RelationErrorCodes, details: RelationErrorDetails) {
+ return (message: string) => {
+ return new RelationError(message, code, details)
+ }
+ }
+
+ constructor(
+ message: string,
+ public readonly code: RelationErrorCodes,
+ public readonly details: RelationErrorDetails,
+ ) {
+ super(message)
+ }
+}
diff --git a/src/errors/OperationError.ts b/src/errors/OperationError.ts
deleted file mode 100644
index 961379a0..00000000
--- a/src/errors/OperationError.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export enum OperationErrorType {
- MissingPrimaryKey = 'MissingPrimaryKey',
- DuplicatePrimaryKey = 'DuplicatePrimaryKey',
- EntityNotFound = 'EntityNotFound',
-}
-
-export class OperationError extends Error {
- public type: ErrorType
-
- constructor(type: ErrorType, message?: string) {
- super(message)
- this.name = 'OperationError'
- this.type = type
- }
-}
diff --git a/src/extensions/index.ts b/src/extensions/index.ts
new file mode 100644
index 00000000..13dd2278
--- /dev/null
+++ b/src/extensions/index.ts
@@ -0,0 +1,21 @@
+import type { Collection } from '#/src/collection.js'
+
+export interface Extension {
+ /**
+ * Name of your extension.
+ */
+ name: string
+
+ /**
+ * A function that is called when your extension is applied by a collection.
+ * @param collection Reference to the `Collection` instance that applies the extension.
+ */
+ extend: (collection: Collection) => void
+}
+
+/**
+ * Creates a new extension.
+ */
+export function defineExtension(options: Extension): Extension {
+ return options
+}
diff --git a/src/extensions/persist.ts b/src/extensions/persist.ts
new file mode 100644
index 00000000..3380c312
--- /dev/null
+++ b/src/extensions/persist.ts
@@ -0,0 +1,182 @@
+import { invariant } from 'outvariant'
+import { unset } from 'es-toolkit/compat'
+import { defineExtension } from '#/src/extensions/index.js'
+import {
+ kCollectionId,
+ kPrimaryKey,
+ kRelationMap,
+ type Collection,
+ type RecordType,
+} from '#/src/collection.js'
+import { Logger } from '#/src/logger.js'
+import type { PropertyPath } from '#/src/utils.js'
+
+const STORAGE_KEY = 'msw/data/storage'
+const METADATA_KEY = '__metadata__'
+
+interface SerializedCollection {
+ version: number
+ collectionId: number
+ records: Array
+}
+
+export interface SerializedRecord {
+ [key: string]: unknown
+ [METADATA_KEY]: RecordMetadata
+}
+
+interface RecordMetadata {
+ primaryKey: string
+ relations: Array<{
+ path: PropertyPath
+ foreignKeys: Array
+ }>
+}
+
+/**
+ * Persists the collection between page reloads.
+ */
+export function persist() {
+ return defineExtension({
+ name: 'persist',
+ async extend(collection) {
+ if (
+ typeof window === 'undefined' ||
+ typeof localStorage === 'undefined'
+ ) {
+ return
+ }
+
+ const logger = new Logger('extension').extend('persist')
+ const COLLECTION_KEY = `${STORAGE_KEY}/${collection[kCollectionId]}`
+
+ // Flush the collection's on page unload.
+ window.addEventListener('unload', () => {
+ localStorage.setItem(
+ COLLECTION_KEY,
+ /**
+ * @fixme Stringifying relations errors because they produce
+ * circular structures. Relations have to be stripped out of the records.
+ * Maybe preserved in the metadata?
+ */
+ JSON.stringify({
+ version: 1,
+ collectionId: collection[kCollectionId],
+ records: collection.all().map(serializeRecord),
+ } satisfies SerializedCollection),
+ )
+ })
+
+ const rawPersistedData = localStorage.getItem(COLLECTION_KEY)
+
+ if (!rawPersistedData) {
+ return
+ }
+ const persistedData = JSON.parse(rawPersistedData) as SerializedCollection
+
+ invariant(
+ persistedData.collectionId === collection[kCollectionId],
+ 'Failed to hydrate data for collection "%s": parsed a state of an unknown collection "%s"',
+ collection[kCollectionId],
+ persistedData.collectionId,
+ )
+
+ logger.log(`found (${persistedData.records.length}) records to hydrate!`)
+
+ await Promise.all(
+ persistedData.records.map(async (serializedRecord) => {
+ logger.log('hydrating record...', { serializedRecord })
+ await createFromSerializedRecord(collection, serializedRecord)
+ }),
+ )
+
+ logger.log('hydration done!', collection.all())
+ },
+ })
+}
+
+export function serializeRecord(record: RecordType): SerializedRecord {
+ const result = structuredClone(record) as any as SerializedRecord
+
+ const metadata: RecordMetadata = {
+ primaryKey: record[kPrimaryKey],
+ relations: [],
+ }
+
+ // Delete relational keys since they can produce non-serializable structures.
+ const relations = record[kRelationMap]
+ for (const [path, relation] of relations) {
+ metadata.relations.push({
+ path: relation.path,
+ foreignKeys: Array.from(relation.foreignKeys),
+ })
+
+ unset(result, path)
+ }
+
+ result[METADATA_KEY] = metadata
+
+ return result
+}
+
+export function deserializeRecord(
+ record: SerializedRecord,
+): Record {
+ const metadata = record[METADATA_KEY]
+
+ invariant(
+ metadata,
+ 'Failed to deserialize record (%j): metadata is missing',
+ record,
+ )
+
+ // Restore the primary key for this record so it's preserved across reloads.
+ Object.defineProperties(record, {
+ [kPrimaryKey]: {
+ enumerable: false,
+ configurable: false,
+ value: metadata.primaryKey,
+ },
+ })
+
+ delete record[METADATA_KEY as keyof typeof record]
+
+ invariant(
+ !(METADATA_KEY in record),
+ 'Failed to deserialize record (%j): metadata not cleared',
+ record,
+ )
+
+ return record
+}
+
+export async function createFromSerializedRecord(
+ collection: Collection,
+ serializedRecord: SerializedRecord,
+): Promise {
+ const metadata = serializedRecord[METADATA_KEY]
+ const initialValues = deserializeRecord(serializedRecord)
+
+ invariant(
+ !(METADATA_KEY in initialValues),
+ 'Failed to create record from deserialized record (%j): metadata not cleared',
+ initialValues,
+ )
+
+ const record: RecordType = await collection.create(initialValues)
+ const relationMap = record[kRelationMap]
+
+ for (const serializedRelation of metadata.relations) {
+ const relation = relationMap.get(serializedRelation.path.join('.'))
+
+ if (relation == null) {
+ continue
+ }
+
+ for (const foreignKey of serializedRelation.foreignKeys) {
+ relation.foreignKeys.add(foreignKey)
+ }
+ }
+
+ return record
+}
diff --git a/src/extensions/sync.ts b/src/extensions/sync.ts
index 90bb4576..44a0420e 100644
--- a/src/extensions/sync.ts
+++ b/src/extensions/sync.ts
@@ -1,134 +1,232 @@
-import { ENTITY_TYPE, PRIMARY_KEY, Entity } from '../glossary'
+import { set } from 'es-toolkit/compat'
+import { defineExtension } from '#/src/extensions/index.js'
import {
- Database,
- DatabaseEventsMap,
- SerializedEntity,
- SERIALIZED_INTERNAL_PROPERTIES_KEY,
-} from '../db/Database'
-import { inheritInternalProperties } from '../utils/inheritInternalProperties'
-
-export type DatabaseMessageEventData =
+ kCollectionId,
+ kPrimaryKey,
+ kRelationMap,
+ type Collection,
+ type RecordType,
+} from '#/src/collection.js'
+import type { Query } from '#/src/query.js'
+import { isObject, type PropertyPath } from '#/src/utils.js'
+import { Logger } from '#/src/logger.js'
+import {
+ serializeRecord,
+ createFromSerializedRecord,
+ type SerializedRecord,
+} from '#/src/extensions/persist.js'
+
+type BroadcastOperation =
| {
- operationType: 'create'
- payload: Parameters
+ type: 'create'
+ senderId: Collection[typeof kCollectionId]
+ record: SerializedRecord
}
| {
- operationType: 'update'
- payload: Parameters
+ type: 'update'
+ senderId: Collection[typeof kCollectionId]
+ primaryKey: string
+ path: PropertyPath
+ nextValue: unknown
}
| {
- operationType: 'delete'
- payload: Parameters
+ type: 'delete'
+ senderId: Collection[typeof kCollectionId]
+ primaryKey: string
}
-function removeListeners(
- event: Event,
- db: Database,
-) {
- const listeners = db.events.listeners(event) as DatabaseEventsMap[Event][]
-
- listeners.forEach((listener) => {
- db.events.removeListener(event, listener)
- })
-
- return () => {
- listeners.forEach((listener) => {
- db.events.addListener(event, listener)
- })
- }
-}
+const BROADCAST_CHANNEL_NAME = 'msw/data/sync'
/**
- * Sets the serialized internal properties as symbols
- * on the given entity.
- * @note `Symbol` properties are stripped off when sending
- * an object over an event emitter.
+ * Synchronizes collection operations (create/update/delete)
+ * with the same collection in another browser tab(s).
*/
-function deserializeEntity(entity: SerializedEntity): Entity {
- const {
- [SERIALIZED_INTERNAL_PROPERTIES_KEY]: internalProperties,
- ...publicProperties
- } = entity
-
- inheritInternalProperties(publicProperties, {
- [ENTITY_TYPE]: internalProperties.entityType,
- [PRIMARY_KEY]: internalProperties.primaryKey,
- })
-
- return publicProperties
-}
-
-/**
- * Synchronizes database operations across multiple clients.
- */
-export function sync(db: Database) {
- const IS_BROWSER = typeof window !== 'undefined'
- const SUPPORTS_BROADCAST_CHANNEL = typeof BroadcastChannel !== 'undefined'
-
- if (!IS_BROWSER || !SUPPORTS_BROADCAST_CHANNEL) {
- return
- }
+export function sync() {
+ const logger = new Logger('extension').extend('sync')
+
+ return defineExtension({
+ name: 'sync',
+ extend(collection) {
+ if (
+ typeof window === 'undefined' ||
+ typeof BroadcastChannel === 'undefined'
+ ) {
+ return
+ }
- const channel = new BroadcastChannel('mswjs/data/sync')
+ const channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME)
+ const hookContext = { skip: false }
- channel.addEventListener(
- 'message',
- (event: MessageEvent) => {
- const [sourceId] = event.data.payload
+ logger.log('applying extension...', { channel })
- // Ignore messages originating from unrelated databases.
- // Useful in case of multiple databases on the same page.
- if (db.id !== sourceId) {
- return
+ const performWithoutBroadcasting = async (
+ callback: () => Promise,
+ ): Promise => {
+ try {
+ hookContext.skip = true
+ await callback()
+ } finally {
+ hookContext.skip = false
+ }
}
- // Remove database event listener for the signaled operation
- // to prevent an infinite loop when applying this operation.
- const restoreListeners = removeListeners(event.data.operationType, db)
-
- // Apply the database operation signaled from another client
- // to the current database instance.
- switch (event.data.operationType) {
- case 'create': {
- const [modelName, entity, customPrimaryKey] = event.data.payload[1]
- db.create(modelName, deserializeEntity(entity), customPrimaryKey)
- break
+ channel.onmessage = async (event: MessageEvent) => {
+ const { data } = event
+
+ if (!isObject(data)) {
+ return
}
- case 'update': {
- const [modelName, prevEntity, nextEntity] = event.data.payload[1]
- db.update(
- modelName,
- deserializeEntity(prevEntity),
- deserializeEntity(nextEntity),
+ logger.log(
+ `sync event from another collection (${event.data.type})`,
+ event,
+ )
+
+ // Ignore sync events from irrelevant collections.
+ // This only works because collection ID is based on the call order
+ // and remains reproducible across runtimes.
+ if (data.senderId !== collection[kCollectionId]) {
+ logger.log(
+ `sender id (${data.senderId}) differs from this collection id (${collection[kCollectionId]}), skipping...`,
)
- break
+ return
}
- default: {
- db[event.data.operationType](...event.data.payload[1])
+ switch (data.type) {
+ case 'create': {
+ logger.warn('creating new record...', data)
+
+ /**
+ * @note Use the `.create()` method to correctly evolve the schema.
+ * This way, non-serializable schemas can survive sync as long as
+ * the initial values are serializable.
+ */
+ await performWithoutBroadcasting(async () => {
+ const record = await createFromSerializedRecord(
+ collection,
+ data.record,
+ )
+
+ /**
+ * @note Extraneous records might not have been associated with their owners
+ * at the time of sync. Manually ensure the owner is referenced in those relations.
+ */
+ record[kRelationMap].forEach((relation) => {
+ relation.foreignCollections.forEach((foreignCollection) => {
+ const foreignRecords = foreignCollection.findMany((q) =>
+ q.where((foreignRecord) => {
+ return relation.foreignKeys.has(
+ foreignRecord[kPrimaryKey],
+ )
+ }),
+ )
+
+ const foreignRelations = foreignRecords.flatMap(
+ (foreignRecord) => {
+ return relation.getRelationsToOwner(foreignRecord)
+ },
+ )
+ foreignRelations.forEach((foreignRelation) => {
+ foreignRelation.foreignKeys.add(record[kPrimaryKey])
+ })
+ })
+ })
+ })
+ break
+ }
+
+ case 'update': {
+ logger.log('updating record...')
+
+ await performWithoutBroadcasting(async () => {
+ await collection.update(
+ (q: Query) =>
+ q.where((record: RecordType) => {
+ return record[kPrimaryKey] === data.primaryKey
+ }),
+ {
+ data(record) {
+ set(record, data.path, data.nextValue)
+ },
+ },
+ )
+ })
+ break
+ }
+
+ case 'delete': {
+ logger.log('deleting record...')
+
+ await performWithoutBroadcasting(async () => {
+ collection.delete((q: Query) =>
+ q.where((record: RecordType) => {
+ return record[kPrimaryKey] === data.primaryKey
+ }),
+ )
+ })
+ break
+ }
+
+ default: {
+ // @ts-expect-error Runtime validation.
+ throw new Error(`Unknown sync event type "${data.type}"`)
+ }
}
}
- // Re-attach database event listeners.
- restoreListeners()
- },
- )
-
- // Broadcast the emitted event from this client
- // to all the other connected clients.
- function broadcastDatabaseEvent(
- operationType: Event,
- ) {
- return (...payload: Parameters) => {
- channel.postMessage({
- operationType,
- payload,
- } as DatabaseMessageEventData)
- }
- }
+ channel.onmessageerror = (event) => {
+ logger.log('sync channel error!', event)
+ }
+
+ const broadcastOperation = (operation: BroadcastOperation): void => {
+ logger.log('broadcasting...', operation)
+ channel.postMessage(operation)
+ }
- db.events.on('create', broadcastDatabaseEvent('create'))
- db.events.on('update', broadcastDatabaseEvent('update'))
- db.events.on('delete', broadcastDatabaseEvent('delete'))
+ collection.hooks.on('create', (event) => {
+ const { record, initialValues } = event.data
+
+ logger.warn(
+ 'record created, should broadcast?',
+ { record, initialValues },
+ !hookContext.skip,
+ )
+
+ if (!hookContext.skip) {
+ broadcastOperation({
+ type: 'create',
+ senderId: collection[kCollectionId],
+ record: serializeRecord(record),
+ })
+ }
+ })
+
+ collection.hooks.on('update', (event) => {
+ const { prevRecord, path, nextValue } = event.data
+ logger.log('record updated, should broadcast?', !hookContext.skip)
+
+ if (!hookContext.skip) {
+ broadcastOperation({
+ type: 'update',
+ senderId: collection[kCollectionId],
+ primaryKey: prevRecord[kPrimaryKey],
+ path,
+ nextValue,
+ })
+ }
+ })
+
+ collection.hooks.on('delete', (event) => {
+ logger.log('record deleted, should broadcast?', !hookContext.skip)
+
+ if (!hookContext.skip) {
+ broadcastOperation({
+ type: 'delete',
+ senderId: collection[kCollectionId],
+ primaryKey: event.data.deletedRecord[kPrimaryKey],
+ })
+ }
+ })
+ },
+ })
}
diff --git a/src/factory.ts b/src/factory.ts
deleted file mode 100644
index 96c368ae..00000000
--- a/src/factory.ts
+++ /dev/null
@@ -1,295 +0,0 @@
-import { format } from 'outvariant'
-import {
- DATABASE_INSTANCE,
- Entity,
- FactoryAPI,
- ModelAPI,
- ModelDefinition,
- ModelDictionary,
- PRIMARY_KEY,
-} from './glossary'
-import { first } from './utils/first'
-import { executeQuery } from './query/executeQuery'
-import { parseModelDefinition } from './model/parseModelDefinition'
-import { createModel } from './model/createModel'
-import { updateEntity } from './model/updateEntity'
-import { OperationError, OperationErrorType } from './errors/OperationError'
-import { Database } from './db/Database'
-import { generateRestHandlers } from './model/generateRestHandlers'
-import {
- generateGraphQLHandlers,
- generateGraphQLSchema,
-} from './model/generateGraphQLHandlers'
-import { sync } from './extensions/sync'
-
-/**
- * Create a database with the given models.
- */
-export function factory(
- dictionary: Dictionary,
-): FactoryAPI {
- const db = new Database(dictionary)
-
- // Initialize database extensions.
- sync(db)
-
- return Object.entries(dictionary).reduce(
- (acc, [modelName, props]) => {
- acc[modelName] = createModelApi(
- dictionary,
- modelName,
- props,
- db,
- )
- return acc
- },
- {
- [DATABASE_INSTANCE]: db,
- },
- )
-}
-
-function createModelApi<
- Dictionary extends ModelDictionary,
- ModelName extends string,
->(
- dictionary: Dictionary,
- modelName: ModelName,
- definition: ModelDefinition,
- db: Database,
-) {
- const parsedModel = parseModelDefinition(dictionary, modelName, definition)
- const { primaryKey } = parsedModel
-
- const api: ModelAPI = {
- create(initialValues = {}) {
- const entity = createModel(
- modelName,
- definition,
- dictionary,
- parsedModel,
- initialValues,
- db,
- )
-
- const entityId = entity[entity[PRIMARY_KEY]] as string
-
- if (!entityId) {
- throw new OperationError(
- OperationErrorType.MissingPrimaryKey,
- format(
- 'Failed to create a "%s" entity: expected the primary key "%s" to have a value, but got: %s',
- modelName,
- primaryKey,
- entityId,
- ),
- )
- }
-
- // Prevent creation of multiple entities with the same primary key value.
- if (db.has(modelName, entityId)) {
- throw new OperationError(
- OperationErrorType.DuplicatePrimaryKey,
- format(
- 'Failed to create a "%s" entity: an entity with the same primary key "%s" ("%s") already exists.',
- modelName,
- entityId,
- entity[PRIMARY_KEY],
- ),
- )
- }
-
- db.create(modelName, entity)
- return entity
- },
- count(query) {
- if (!query) {
- return db.count(modelName)
- }
-
- const results = executeQuery(modelName, primaryKey, query, db)
- return results.length
- },
- findFirst(query) {
- const results = executeQuery(modelName, primaryKey, query, db)
- const firstResult = first(results)
-
- if (query.strict && firstResult == null) {
- throw new OperationError(
- OperationErrorType.EntityNotFound,
- format(
- 'Failed to execute "findFirst" on the "%s" model: no entity found matching the query "%j".',
- modelName,
- query.where,
- ),
- )
- }
-
- return firstResult as Entity
- },
- findMany(query) {
- const results = executeQuery(modelName, primaryKey, query, db)
-
- if (results.length === 0 && query.strict) {
- throw new OperationError(
- OperationErrorType.EntityNotFound,
- format(
- 'Failed to execute "findMany" on the "%s" model: no entities found matching the query "%j".',
- modelName,
- query.where,
- ),
- )
- }
-
- return results
- },
- getAll() {
- return db.listEntities(modelName)
- },
- update({ strict, ...query }) {
- const results = executeQuery(modelName, primaryKey, query, db)
- const prevRecord = first(results)
-
- if (!prevRecord) {
- if (strict) {
- throw new OperationError(
- OperationErrorType.EntityNotFound,
- format(
- 'Failed to execute "update" on the "%s" model: no entity found matching the query "%j".',
- modelName,
- query.where,
- ),
- )
- }
-
- return null as any
- }
-
- const nextRecord = updateEntity(prevRecord, query.data, definition)
-
- if (
- nextRecord[prevRecord[PRIMARY_KEY]] !==
- prevRecord[prevRecord[PRIMARY_KEY]]
- ) {
- if (db.has(modelName, nextRecord[prevRecord[PRIMARY_KEY]])) {
- throw new OperationError(
- OperationErrorType.DuplicatePrimaryKey,
- format(
- 'Failed to execute "update" on the "%s" model: the entity with a primary key "%s" ("%s") already exists.',
- modelName,
- nextRecord[prevRecord[PRIMARY_KEY]],
- primaryKey,
- ),
- )
- }
- }
-
- db.update(modelName, prevRecord, nextRecord)
-
- return nextRecord
- },
- updateMany({ strict, ...query }) {
- const records = executeQuery(modelName, primaryKey, query, db)
- const updatedRecords: Entity[] = []
-
- if (records.length === 0) {
- if (strict) {
- throw new OperationError(
- OperationErrorType.EntityNotFound,
- format(
- 'Failed to execute "updateMany" on the "%s" model: no entities found matching the query "%j".',
- modelName,
- query.where,
- ),
- )
- }
-
- return null as any
- }
-
- records.forEach((prevRecord) => {
- const nextRecord = updateEntity(prevRecord, query.data, definition)
-
- if (
- nextRecord[prevRecord[PRIMARY_KEY]] !==
- prevRecord[prevRecord[PRIMARY_KEY]]
- ) {
- if (db.has(modelName, nextRecord[prevRecord[PRIMARY_KEY]])) {
- throw new OperationError(
- OperationErrorType.DuplicatePrimaryKey,
- format(
- 'Failed to execute "updateMany" on the "%s" model: the entity with a primary key "%s" ("%s") already exists.',
- modelName,
- nextRecord[prevRecord[PRIMARY_KEY]],
- primaryKey,
- ),
- )
- }
- }
-
- db.update(modelName, prevRecord, nextRecord)
- updatedRecords.push(nextRecord)
- })
-
- return updatedRecords
- },
- delete({ strict, ...query }) {
- const results = executeQuery(modelName, primaryKey, query, db)
- const record = first(results)
-
- if (!record) {
- if (strict) {
- throw new OperationError(
- OperationErrorType.EntityNotFound,
- format(
- 'Failed to execute "delete" on the "%s" model: no entity found matching the query "%o".',
- modelName,
- query.where,
- ),
- )
- }
-
- return null as any
- }
-
- db.delete(modelName, record[record[PRIMARY_KEY]] as string)
- return record
- },
- deleteMany({ strict, ...query }) {
- const records = executeQuery(modelName, primaryKey, query, db)
-
- if (records.length === 0) {
- if (strict) {
- throw new OperationError(
- OperationErrorType.EntityNotFound,
- format(
- 'Failed to execute "deleteMany" on the "%s" model: no entities found matching the query "%o".',
- modelName,
- query.where,
- ),
- )
- }
-
- return null as any
- }
-
- records.forEach((record) => {
- db.delete(modelName, record[record[PRIMARY_KEY]] as string)
- })
-
- return records
- },
- toHandlers(type: 'rest' | 'graphql', baseUrl: string): any {
- if (type === 'graphql') {
- return generateGraphQLHandlers(modelName, definition, api, baseUrl)
- }
-
- return generateRestHandlers(modelName, definition, api, baseUrl)
- },
- toGraphQLSchema() {
- return generateGraphQLSchema(modelName, definition, api)
- },
- }
-
- return api
-}
diff --git a/src/glossary.ts b/src/glossary.ts
deleted file mode 100644
index 63326aba..00000000
--- a/src/glossary.ts
+++ /dev/null
@@ -1,244 +0,0 @@
-import { GraphQLSchema } from 'graphql'
-import { GraphQLHandler, RestHandler } from 'msw'
-import { Database } from './db/Database'
-import { NullableProperty } from './nullable'
-import { PrimaryKey } from './primaryKey'
-import {
- BulkQueryOptions,
- QueryOptions,
- QuerySelector,
- WeakQuerySelector,
-} from './query/queryTypes'
-import { OneOf, ManyOf } from './relations/Relation'
-
-export const PRIMARY_KEY = Symbol('primaryKey')
-export const ENTITY_TYPE = Symbol('type')
-export const DATABASE_INSTANCE = Symbol('databaseInstance')
-
-export type KeyType = string | number | symbol
-export type AnyObject = Record
-export type PrimaryKeyType = string | number
-export type PrimitiveValueType = string | number | boolean | Date
-export type ModelValueType = PrimitiveValueType | PrimitiveValueType[]
-export type ModelValueTypeGetter = () => ModelValueType
-
-export type ModelDefinition = Record
-
-export type ModelDefinitionValue =
- | PrimaryKey
- | ModelValueTypeGetter
- | NullableProperty
- | OneOf
- | ManyOf
- | NestedModelDefinition
-
-export type NestedModelDefinition = {
- [propertyName: string]:
- | ModelValueTypeGetter
- | NullableProperty
- | OneOf
- | ManyOf
- | NestedModelDefinition
-}
-
-export type FactoryAPI> = {
- [ModelName in keyof Dictionary]: ModelAPI
-} & {
- [DATABASE_INSTANCE]: Database
-}
-
-export type ModelDictionary = Record>
-
-export type Limit = {
- [Key in keyof Definition]: Definition[Key] extends ModelDefinitionValue
- ? Definition[Key]
- : {
- error: 'expected primary key, initial value, or relation'
- }
-}
-
-export interface InternalEntityProperties {
- readonly [ENTITY_TYPE]: ModelName
- readonly [PRIMARY_KEY]: PrimaryKeyType
-}
-
-export type Entity<
- Dictionary extends ModelDictionary,
- ModelName extends keyof Dictionary,
-> = PublicEntity & InternalEntityProperties
-
-export type PublicEntity<
- Dictionary extends ModelDictionary,
- ModelName extends keyof Dictionary,
-> = Value
-
-export type RequiredExactlyOne<
- ObjectType,
- KeysType extends keyof ObjectType = keyof ObjectType,
-> = {
- [Key in KeysType]: Required> &
- Partial, never>>
-}[KeysType] &
- Pick>
-
-export type DeepRequiredExactlyOne =
- RequiredExactlyOne<{
- [Key in keyof Target]: Target[Key] extends AnyObject
- ? DeepRequiredExactlyOne
- : Target[Key]
- }>
-
-export type InitialValues<
- Dictionary extends ModelDictionary,
- ModelName extends keyof Dictionary,
-> = Partial>
-
-export type StrictQueryReturnType<
- Options extends QueryOptions,
- ValueType extends unknown,
-> = Options['strict'] extends true ? ValueType : ValueType | null
-
-export interface ModelAPI<
- Dictionary extends ModelDictionary,
- ModelName extends keyof Dictionary,
-> {
- /**
- * Create a single entity for the model.
- */
- create(
- initialValues?: InitialValues,
- ): Entity
- /**
- * Return the total number of entities.
- */
- count(
- query?: QueryOptions & QuerySelector>,
- ): number
- /**
- * Find a first entity matching the query.
- */
- findFirst(
- query: Options & QuerySelector>,
- ): StrictQueryReturnType>
- /**
- * Find multiple entities.
- */
- findMany(
- query: QueryOptions &
- WeakQuerySelector> &
- BulkQueryOptions>,
- ): Entity[]
- /**
- * Return all entities of the current model.
- */
- getAll(): Entity[]
- /**
- * Update a single entity with the next data.
- */
- update(
- query: Options &
- QuerySelector> & {
- data: Partial>
- },
- ): StrictQueryReturnType>
- /**
- * Update many entities with the next data.
- */
- updateMany(
- query: Options &
- QuerySelector> & {
- data: Partial>
- },
- ): StrictQueryReturnType[]>
- /**
- * Delete a single entity.
- */
- delete(
- query: Options & QuerySelector>,
- ): StrictQueryReturnType>
- /**
- * Delete multiple entities.
- */
- deleteMany(
- query: Options & QuerySelector>,
- ): StrictQueryReturnType[]>
- /**
- * Generate request handlers of the given type based on the model definition.
- */
- toHandlers(type: 'rest', baseUrl?: string): RestHandler[]
- /**
- * Generate request handlers of the given type based on the model definition.
- */
- toHandlers(type: 'graphql', baseUrl?: string): GraphQLHandler[]
-
- /**
- * Generate a graphql schema based on the model definition.
- */
- toGraphQLSchema(): GraphQLSchema
-}
-
-export type UpdateManyValue<
- Target extends AnyObject,
- Dictionary extends ModelDictionary,
- ModelRoot extends AnyObject = Target,
-> =
- | Value
- | {
- [Key in keyof Target]?: Target[Key] extends PrimaryKey
- ? (
- prevValue: ReturnType,
- entity: Value,
- ) => ReturnType
- : Target[Key] extends ModelValueTypeGetter
- ? (
- prevValue: ReturnType,
- entity: Value,
- ) => ReturnType
- : Target[Key] extends OneOf
- ? (
- prevValue: PublicEntity,
- entity: Value,
- ) => PublicEntity
- : Target[Key] extends ManyOf
- ? (
- prevValue: PublicEntity[],
- entity: Value,
- ) => PublicEntity[]
- : Target[Key] extends AnyObject
- ? Partial>
- : (
- prevValue: ReturnType,
- entity: Value,
- ) => ReturnType
- }
-
-export type Value<
- Target extends AnyObject,
- Dictionary extends ModelDictionary,
-> = {
- [Key in keyof Target]: Target[Key] extends PrimaryKey
- ? ReturnType
- : // Extract underlying value type of nullable properties
- Target[Key] extends NullableProperty
- ? ReturnType
- : // Extract value type from OneOf relations.
- Target[Key] extends OneOf
- ? Nullable extends true
- ? PublicEntity | null
- : PublicEntity | undefined
- : // Extract value type from ManyOf relations.
- Target[Key] extends ManyOf
- ? Nullable extends true
- ? PublicEntity[] | null
- : PublicEntity