miragejs-orm 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1780 -9
- package/lib/dist/index.cjs +4 -4
- package/lib/dist/index.cjs.map +1 -1
- package/lib/dist/index.d.cts +1928 -1450
- package/lib/dist/index.d.ts +1928 -1450
- package/lib/dist/index.js +4 -4
- package/lib/dist/index.js.map +1 -1
- package/package.json +30 -27
package/README.md
CHANGED
|
@@ -1,17 +1,1788 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<img src="./docs/logo.svg" alt="MirageJS Logo" width="100" />
|
|
4
|
+
|
|
5
|
+
# MirageJS ORM
|
|
6
|
+
|
|
7
|
+
> A TypeScript-first ORM for building in-memory databases with models, relationships, and factories
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/miragejs-orm)
|
|
10
|
+
[](https://bundlephobia.com/package/miragejs-orm)
|
|
11
|
+
[](./LICENSE)
|
|
12
|
+
[]()
|
|
13
|
+
[](https://www.typescriptlang.org/)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## ✨ What is miragejs-orm?
|
|
20
|
+
|
|
21
|
+
**miragejs-orm** is a complete TypeScript rewrite of the powerful ORM layer from MirageJS, designed to give frontend developers the freedom to quickly create type-safe mocks for both testing and development — without backend dependencies.
|
|
22
|
+
|
|
23
|
+
Build realistic, relational data models in memory with factories, traits, relationships, and serialization, all with **100% type safety** and a modern, fluent API.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 🚀 Why Choose miragejs-orm?
|
|
28
|
+
|
|
29
|
+
### Compared to MirageJS
|
|
30
|
+
|
|
31
|
+
While MirageJS is an excellent solution for full API mocking, `miragejs-orm` takes the most powerful part - the ORM — and enhances it:
|
|
32
|
+
|
|
33
|
+
- **🎯 Fully Rewritten in TypeScript** - Built from the ground up with TypeScript, providing complete type safety and excellent IDE autocomplete
|
|
34
|
+
- **🪶 Zero Dependencies** - No external dependencies means smaller bundle size (~55KB) and no supply chain concerns
|
|
35
|
+
- **🔌 Framework Agnostic** - Use with any HTTP interceptor library (MSW, Mirage Server, Axios interceptors, etc.) or testing framework
|
|
36
|
+
- **⚡ Modern Fluent API** - Declarative builder patterns let you construct schemas, models, and factories with an intuitive, chainable API
|
|
37
|
+
- **📦 No Inflection Magic Under The Hood** - Now you control exactly how your model names and attributes are formatted - what you define is what you get
|
|
38
|
+
- **✅ Battle Tested** - 900+ test cases with 95% code coverage, including type tests, ensure reliability
|
|
39
|
+
- **🔧 Modern Tooling** - Built with modern build tools and package standards for optimal developer experience
|
|
40
|
+
|
|
41
|
+
### Key Benefits
|
|
42
|
+
|
|
43
|
+
- **Develop UI-First** - Don't wait for backend APIs. Build complete frontend features with realistic data
|
|
44
|
+
- **Flexible Data Modeling** - Create models that mirror your backend entities OR design custom models for specific endpoints
|
|
45
|
+
- **Built-in Serialization** - Transform your data on output with serializers to match API formats, hide sensitive fields, and control response structure
|
|
46
|
+
- **Type-Safe Mocking** - Full TypeScript support means your mocks stay in sync with your types
|
|
47
|
+
- **Testing & Development** - Perfect for unit tests, integration tests, Storybook stories, and development environments
|
|
48
|
+
|
|
49
|
+
**📂 Example project:** See the [task-board example](./examples/task-board) for a full reference: schema setup, models, collections, relations, factories, seeds, serializers, and MSW handlers. Use it to learn the library and as a pattern for your own projects.
|
|
4
50
|
|
|
5
51
|
---
|
|
6
52
|
|
|
7
|
-
##
|
|
53
|
+
## 💭 Philosophy
|
|
54
|
+
|
|
55
|
+
### Freedom Over Rigidity
|
|
56
|
+
|
|
57
|
+
The core idea behind `miragejs-orm` is to give frontend developers a **playground, not a prison**. We don't force you to perfectly replicate your backend architecture - instead, we give you the tools to create exactly what you need:
|
|
8
58
|
|
|
9
|
-
|
|
59
|
+
- **Model Your API, Your Way** – Build a complete relational model that mirrors your server, OR create minimal models for specific endpoint outputs
|
|
60
|
+
- **No Scope Creep** – Keep your mock data within the library's scope rather than managing complex state in route handlers or test setup
|
|
61
|
+
- **UI-First Development** – Get ahead of backend development and prototype features with realistic, relational data
|
|
10
62
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
63
|
+
### Schema-less but Type-Safe
|
|
64
|
+
|
|
65
|
+
We embrace a unique philosophy:
|
|
66
|
+
|
|
67
|
+
- **No Runtime Validation** – Models are schema-less by design. You're responsible for keeping your test data correct so tests meet expectations
|
|
68
|
+
- **100% Type Safety** – On our side, we provide complete TypeScript support to make mock management fully type-safe
|
|
69
|
+
- **Developer Freedom** – We give you powerful tools without imposing backend-style validation constraints
|
|
70
|
+
|
|
71
|
+
This approach means faster iteration, simpler setup, and complete control over your mock data while maintaining the benefits of TypeScript's compile-time safety.
|
|
16
72
|
|
|
17
73
|
---
|
|
74
|
+
|
|
75
|
+
<div align="center">
|
|
76
|
+
|
|
77
|
+
# 📖 Quick Guide
|
|
78
|
+
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
## 📦 Installation
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm install miragejs-orm
|
|
85
|
+
|
|
86
|
+
# or
|
|
87
|
+
yarn add miragejs-orm
|
|
88
|
+
|
|
89
|
+
# or
|
|
90
|
+
pnpm add miragejs-orm
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 🏃 Quick Start
|
|
96
|
+
|
|
97
|
+
Here's a taste of what you can do with `miragejs-orm`:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { model, schema, collection, factory, relations } from 'miragejs-orm';
|
|
101
|
+
|
|
102
|
+
// 1. Define your models
|
|
103
|
+
const userModel = model('user', 'users')
|
|
104
|
+
.attrs<{ name: string; email: string }>()
|
|
105
|
+
.build();
|
|
106
|
+
|
|
107
|
+
const postModel = model('post', 'posts')
|
|
108
|
+
.attrs<{ title: string; content: string; authorId: string }>()
|
|
109
|
+
.build();
|
|
110
|
+
|
|
111
|
+
// 2. Create factories with fake data
|
|
112
|
+
const userFactory = factory()
|
|
113
|
+
.model(userModel)
|
|
114
|
+
.attrs({
|
|
115
|
+
name: () => 'John Doe',
|
|
116
|
+
email: () => 'john@example.com',
|
|
117
|
+
})
|
|
118
|
+
.build();
|
|
119
|
+
|
|
120
|
+
// 3. Setup your schema with relationships
|
|
121
|
+
const testSchema = schema()
|
|
122
|
+
.collections({
|
|
123
|
+
users: collection()
|
|
124
|
+
.model(userModel)
|
|
125
|
+
.factory(userFactory)
|
|
126
|
+
.relationships({
|
|
127
|
+
posts: relations.hasMany(postModel),
|
|
128
|
+
})
|
|
129
|
+
.build(),
|
|
130
|
+
|
|
131
|
+
posts: collection()
|
|
132
|
+
.model(postModel)
|
|
133
|
+
.relationships({
|
|
134
|
+
author: relations.belongsTo(userModel, { foreignKey: 'authorId' }),
|
|
135
|
+
})
|
|
136
|
+
.build(),
|
|
137
|
+
})
|
|
138
|
+
.build();
|
|
139
|
+
|
|
140
|
+
// 4. Use it!
|
|
141
|
+
const user = testSchema.users.create({ name: 'Alice' });
|
|
142
|
+
|
|
143
|
+
const post = testSchema.posts.create({
|
|
144
|
+
title: 'Hello World',
|
|
145
|
+
content: 'My first post',
|
|
146
|
+
authorId: user.id,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
console.log(user.posts.length); // 1
|
|
150
|
+
console.log(post.author.name); // 'Alice'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 📚 Core Concepts
|
|
156
|
+
|
|
157
|
+
### 1. Model Templates
|
|
158
|
+
|
|
159
|
+
**Model Templates** define the structure of your data entities. They're created using the `model()` builder and are schema-less at runtime but fully typed at compile time.
|
|
160
|
+
|
|
161
|
+
Model Templates are designed to be **shareable across your schema** - you can reference the same template when setting up relationships and collections, ensuring consistent type inference throughout your application.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
import { model } from 'miragejs-orm';
|
|
165
|
+
|
|
166
|
+
// Define your model attributes interface
|
|
167
|
+
interface UserAttrs {
|
|
168
|
+
name: string;
|
|
169
|
+
email: string;
|
|
170
|
+
role?: string;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Create a model template
|
|
174
|
+
const userModel = model('user', 'users').attrs<UserAttrs>().build();
|
|
175
|
+
|
|
176
|
+
// This template can now be shared across collections and relationships
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
<details>
|
|
180
|
+
<summary><strong>Key points</strong></summary>
|
|
181
|
+
|
|
182
|
+
- Model Templates are the building blocks created by the `model()` builder.
|
|
183
|
+
- First argument is the **model name** (singular), second is the **collection name** (plural).
|
|
184
|
+
- Use `.attrs<T>()` to define the TypeScript interface for your model.
|
|
185
|
+
- **JavaScript users** — You can pass a plain object to `.attrs()` instead of a generic (e.g. `.attrs({ name: '', email: '' })`). The object shape gives the IDE good IntelliSense for attributes without TypeScript.
|
|
186
|
+
- Templates are **shareable** — use the same template reference for relationships and type inference.
|
|
187
|
+
- Models are immutable once created.
|
|
188
|
+
|
|
189
|
+
</details>
|
|
190
|
+
|
|
191
|
+
### 2. Collections
|
|
192
|
+
|
|
193
|
+
Collections are containers for models that live in your schema. They handle CRUD operations and queries.
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { collection, relations } from 'miragejs-orm';
|
|
197
|
+
|
|
198
|
+
const userCollection = collection()
|
|
199
|
+
.model(userModel)
|
|
200
|
+
.factory(userFactory) // Optional
|
|
201
|
+
.relationships({ posts: relations.hasMany(postModel) }) // Optional
|
|
202
|
+
.serializer(userSerializer) // Optional
|
|
203
|
+
.build();
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Creating**
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// New (in-memory only, not saved to DB — uses only the attributes you pass, no factory)
|
|
210
|
+
const user = testSchema.users.new({
|
|
211
|
+
name: 'Alice',
|
|
212
|
+
email: 'alice@example.com',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Create with custom attributes
|
|
216
|
+
const user = testSchema.users.create({
|
|
217
|
+
name: 'Alice',
|
|
218
|
+
email: 'alice@example.com',
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Create with factory traits
|
|
222
|
+
const adminUser = testSchema.users.create({ name: 'Admin' }, 'admin');
|
|
223
|
+
|
|
224
|
+
// Create multiple identical records
|
|
225
|
+
const users = testSchema.users.createMany(3);
|
|
226
|
+
|
|
227
|
+
// Create multiple different records: two regular users and one admin
|
|
228
|
+
const users = testSchema.users.createMany([
|
|
229
|
+
[{ name: 'Alice', email: 'alice@example.com' }],
|
|
230
|
+
[{ name: 'Bob', email: 'bob@example.com' }],
|
|
231
|
+
['admin'], // Using a trait
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
// Find or create by attributes
|
|
235
|
+
const user = testSchema.users.findOrCreateBy(
|
|
236
|
+
{ email: 'alice@example.com' },
|
|
237
|
+
{ name: 'Alice', role: 'user' },
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Find many or create by attributes
|
|
241
|
+
const users = testSchema.users.findManyOrCreateBy(
|
|
242
|
+
5,
|
|
243
|
+
{ role: 'user' },
|
|
244
|
+
{ isActive: true },
|
|
245
|
+
);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Querying**
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
// Find by ID
|
|
252
|
+
const user = testSchema.users.find('1');
|
|
253
|
+
|
|
254
|
+
// Find with conditions
|
|
255
|
+
const admin = testSchema.users.find({ where: { role: 'admin' } });
|
|
256
|
+
|
|
257
|
+
// Find many by IDs
|
|
258
|
+
const users = testSchema.users.findMany(['1', '2', '3']);
|
|
259
|
+
|
|
260
|
+
// Find with predicate function
|
|
261
|
+
const activeUsers = testSchema.users.findMany({
|
|
262
|
+
where: (user) => user.isActive && user.role === 'admin',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Get all records
|
|
266
|
+
const allUsers = testSchema.users.all();
|
|
267
|
+
|
|
268
|
+
// Get first / last / by index
|
|
269
|
+
const first = testSchema.users.first();
|
|
270
|
+
const last = testSchema.users.last();
|
|
271
|
+
const thirdUser = testSchema.users.at(2);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Record management (update & remove)**
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// Update a record (model method)
|
|
278
|
+
user.update({ name: 'Bob', role: 'admin' });
|
|
279
|
+
|
|
280
|
+
// Delete a record (model method)
|
|
281
|
+
user.destroy();
|
|
282
|
+
|
|
283
|
+
// Delete by ID (collection method)
|
|
284
|
+
testSchema.users.delete('1');
|
|
285
|
+
|
|
286
|
+
// Delete multiple records (collection method)
|
|
287
|
+
testSchema.users.deleteMany(['1', '2', '3']);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
<details>
|
|
291
|
+
<summary><strong>Key points</strong></summary>
|
|
292
|
+
|
|
293
|
+
- **Creating and finding** — Use collection methods: `create()`, `createMany()`, `find()`, `findMany()`, `findOrCreateBy()`, `findManyOrCreateBy()`, `all()`, `first()`, `last()`, `at()`.
|
|
294
|
+
- **Updating and removing** — Use model methods: `model.update()`, `model.destroy()`, or collection helpers `collection.delete(id)` / `collection.deleteMany(ids)`.
|
|
295
|
+
- **`new()`** — Creates a model instance in memory only (not saved to the DB), using only the attributes you pass (no factory). Exists mainly for internal use but is exposed for parity with the legacy MirageJS API. Prefer `create()` for building and persisting models in normal application or test code.
|
|
296
|
+
- **Database (db) API** — Operating on raw records via `schema.db.collectionName` is not recommended for normal use. The db API is exposed mainly for debugging and low-level access.
|
|
297
|
+
- **Naming conventions** — The API uses consistent singular/plural and query patterns:
|
|
298
|
+
- **Singular/plural:** `create()` / `createMany()`, `find()` / `findMany()`, `delete()` / `deleteMany()`.
|
|
299
|
+
- **Query API:** Use `find({ where })` and `findMany({ where })` instead of separate `findBy` / `where`-style APIs.
|
|
300
|
+
- **Find-or-create:** `findOrCreateBy()` and `findManyOrCreateBy()` for conditional creation.
|
|
301
|
+
|
|
302
|
+
</details>
|
|
303
|
+
|
|
304
|
+
### 3. Relationships
|
|
305
|
+
|
|
306
|
+
Define relationships between models using **relations** in your collection configuration. Relations are used only for **schema relationship definitions**. For automatically creating or linking related records in factories, use **associations** (see Factory Associations).
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { relations } from 'miragejs-orm';
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
#### HasMany Relationship
|
|
313
|
+
|
|
314
|
+
Use `relations.hasMany()` to define a one-to-many relationship where a model has multiple related records.
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// In users collection - define the relationship
|
|
318
|
+
relationships: {
|
|
319
|
+
posts: relations.hasMany(postModel),
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Usage - access related records
|
|
323
|
+
const post = testSchema.posts.create({ title: 'Hello' });
|
|
324
|
+
const user = testSchema.users.create({ name: 'Alice', posts: [post] });
|
|
325
|
+
|
|
326
|
+
console.log(user.posts); // ModelCollection with the post
|
|
327
|
+
console.log(user.posts.length); // 1
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### BelongsTo Relationship
|
|
331
|
+
|
|
332
|
+
Use `relations.belongsTo()` to define a many-to-one relationship where a model belongs to another model.
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// In posts collection - define the relationship
|
|
336
|
+
relationships: {
|
|
337
|
+
author: relations.belongsTo(userModel, { foreignKey: 'authorId' }),
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Usage - access the parent record
|
|
341
|
+
const post = testSchema.posts.find('1');
|
|
342
|
+
console.log(post.author.name); // Access related user
|
|
343
|
+
console.log(post.authorId); // The foreign key value
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
#### Many-to-Many Relationships
|
|
347
|
+
|
|
348
|
+
For many-to-many relationships, use `relations.hasMany()` on both sides with array foreign keys.
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { model, schema, collection, relations } from 'miragejs-orm';
|
|
352
|
+
|
|
353
|
+
const studentModel = model('student', 'students')
|
|
354
|
+
.attrs<{ name: string; courseIds: string[] }>()
|
|
355
|
+
.build();
|
|
356
|
+
|
|
357
|
+
const courseModel = model('course', 'courses')
|
|
358
|
+
.attrs<{ title: string; studentIds: string[] }>()
|
|
359
|
+
.build();
|
|
360
|
+
|
|
361
|
+
const testSchema = schema()
|
|
362
|
+
.collections({
|
|
363
|
+
students: collection()
|
|
364
|
+
.model(studentModel)
|
|
365
|
+
.relationships({
|
|
366
|
+
courses: relations.hasMany(courseModel, {
|
|
367
|
+
foreignKey: 'courseIds',
|
|
368
|
+
}),
|
|
369
|
+
})
|
|
370
|
+
.build(),
|
|
371
|
+
|
|
372
|
+
courses: collection()
|
|
373
|
+
.model(courseModel)
|
|
374
|
+
.relationships({
|
|
375
|
+
students: relations.hasMany(studentModel, {
|
|
376
|
+
foreignKey: 'studentIds',
|
|
377
|
+
}),
|
|
378
|
+
})
|
|
379
|
+
.build(),
|
|
380
|
+
})
|
|
381
|
+
.build();
|
|
382
|
+
|
|
383
|
+
// Usage - bidirectional access
|
|
384
|
+
const student = testSchema.students.create({
|
|
385
|
+
name: 'Alice',
|
|
386
|
+
courseIds: ['1', '2'],
|
|
387
|
+
});
|
|
388
|
+
console.log(student.courses.length); // 2
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
<details>
|
|
392
|
+
<summary><strong>Key points</strong></summary>
|
|
393
|
+
|
|
394
|
+
- **Same model template** — For correct behavior, the model template passed to the collection (`.model(...)`) and to the relationship utilities (`relations.hasMany(...)`, `relations.belongsTo(...)`) must be the same reference. Mismatches can break type inference and relationship resolution.
|
|
395
|
+
- **Default foreign key** — The default foreign key is derived from the **target model’s name** (from its template): `belongsTo(userModel)` → `userId`; `hasMany(postModel)` → `postIds`. You don’t need to pass `foreignKey` when your attribute matches this default.
|
|
396
|
+
- **Relationship name vs foreign key** — The relationship key (e.g. `author`) is independent of the stored attribute. If you want a different relationship name but the default FK is based on the target model name, specify `foreignKey` explicitly. Example: `author: relations.belongsTo(userModel, { foreignKey: 'authorId' })` uses the key `author` but stores the ID in `authorId`; without `foreignKey`, the default would be `userId` (from `userModel.modelName`).
|
|
397
|
+
|
|
398
|
+
</details>
|
|
399
|
+
|
|
400
|
+
### 4. Factories
|
|
401
|
+
|
|
402
|
+
Factories help you generate realistic test data with minimal boilerplate.
|
|
403
|
+
|
|
404
|
+
#### Basic Factory
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { factory } from 'miragejs-orm';
|
|
408
|
+
import { faker } from '@faker-js/faker';
|
|
409
|
+
|
|
410
|
+
const userFactory = factory()
|
|
411
|
+
.model(userModel)
|
|
412
|
+
.attrs({
|
|
413
|
+
name: () => faker.person.fullName(),
|
|
414
|
+
email: () => faker.internet.email(),
|
|
415
|
+
role: 'user', // Static default
|
|
416
|
+
})
|
|
417
|
+
.build();
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
#### Derived attributes with resolveFactoryAttr
|
|
421
|
+
|
|
422
|
+
When one attribute depends on another (e.g. email from name), use the **`resolveFactoryAttr`** helper inside an attr function. It resolves another attr:
|
|
423
|
+
|
|
424
|
+
- if that attr is a function, it calls it with the current model id;
|
|
425
|
+
- if it's a static value, it returns it.
|
|
426
|
+
|
|
427
|
+
That way you don't have to write the branching yourself: **it replaces** manual checks like `typeof this.name === 'function' ? this.name(id) : this.name` and keeps attr functions readable when they depend on other attrs.
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
import { factory, resolveFactoryAttr } from 'miragejs-orm';
|
|
431
|
+
import { faker } from '@faker-js/faker';
|
|
432
|
+
|
|
433
|
+
const userFactory = factory()
|
|
434
|
+
.model(userModel)
|
|
435
|
+
.attrs({
|
|
436
|
+
name: () => faker.person.fullName(),
|
|
437
|
+
email(id) {
|
|
438
|
+
const name = resolveFactoryAttr(this.name, id);
|
|
439
|
+
return `${name.split(' ').join('.').toLowerCase()}@example.com`;
|
|
440
|
+
},
|
|
441
|
+
role: 'user',
|
|
442
|
+
})
|
|
443
|
+
.build();
|
|
444
|
+
|
|
445
|
+
// Creates e.g. { name: 'John Doe', email: 'john.doe@example.com', role: 'user' }
|
|
446
|
+
testSchema.users.create();
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
#### Traits
|
|
450
|
+
|
|
451
|
+
Traits allow you to create variations of your factory:
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
import { factory, associations } from 'miragejs-orm';
|
|
455
|
+
import { faker } from '@faker-js/faker';
|
|
456
|
+
|
|
457
|
+
const userFactory = factory()
|
|
458
|
+
.model(userModel)
|
|
459
|
+
.attrs({
|
|
460
|
+
name: () => faker.person.fullName(),
|
|
461
|
+
email: () => faker.internet.email(),
|
|
462
|
+
role: 'user',
|
|
463
|
+
})
|
|
464
|
+
.traits({
|
|
465
|
+
admin: {
|
|
466
|
+
role: 'admin',
|
|
467
|
+
},
|
|
468
|
+
verified: {
|
|
469
|
+
emailVerified: true,
|
|
470
|
+
afterCreate(model) {
|
|
471
|
+
// Custom logic after creation
|
|
472
|
+
model.update({ verifiedAt: new Date().toISOString() });
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
withPosts: {
|
|
476
|
+
posts: associations.createMany(postModel, 3),
|
|
477
|
+
},
|
|
478
|
+
})
|
|
479
|
+
.build();
|
|
480
|
+
|
|
481
|
+
// Usage
|
|
482
|
+
testSchema.users.create(); // Regular user
|
|
483
|
+
testSchema.users.create('admin'); // Admin user
|
|
484
|
+
testSchema.users.create('admin', 'verified'); // Admin + verified
|
|
485
|
+
testSchema.users.create('withPosts'); // User with 3 posts
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### Factory Associations
|
|
489
|
+
|
|
490
|
+
Create related models automatically:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
import { factory, associations } from 'miragejs-orm';
|
|
494
|
+
|
|
495
|
+
const userFactory = factory()
|
|
496
|
+
.model(userModel)
|
|
497
|
+
.associations({
|
|
498
|
+
// Create 3 identical posts
|
|
499
|
+
posts: associations.createMany(postModel, 3),
|
|
500
|
+
})
|
|
501
|
+
.traits({
|
|
502
|
+
withProfile: {
|
|
503
|
+
profile: associations.create(profileModel),
|
|
504
|
+
},
|
|
505
|
+
})
|
|
506
|
+
.build();
|
|
507
|
+
|
|
508
|
+
// Create multiple different related models
|
|
509
|
+
const authorFactory = factory()
|
|
510
|
+
.model(userModel)
|
|
511
|
+
.associations({
|
|
512
|
+
posts: associations.createMany(postModel, [
|
|
513
|
+
[{ title: 'First Post', published: true }],
|
|
514
|
+
[{ title: 'Draft Post', published: false }],
|
|
515
|
+
['featured'], // Using a trait
|
|
516
|
+
]),
|
|
517
|
+
})
|
|
518
|
+
.build();
|
|
519
|
+
|
|
520
|
+
// Link to existing models (or create if missing)
|
|
521
|
+
const userFactory2 = factory()
|
|
522
|
+
.model(userModel)
|
|
523
|
+
.traits({
|
|
524
|
+
withExistingPost: {
|
|
525
|
+
post: associations.link(postModel), // Finds first existing post, creates one if none exist
|
|
526
|
+
},
|
|
527
|
+
withExistingPosts: {
|
|
528
|
+
posts: associations.linkMany(postModel, 3), // Finds/creates up to 3 posts
|
|
529
|
+
},
|
|
530
|
+
})
|
|
531
|
+
.build();
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
#### Lifecycle Hooks
|
|
535
|
+
|
|
536
|
+
Execute logic after model creation:
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
import { factory } from 'miragejs-orm';
|
|
540
|
+
import { faker } from '@faker-js/faker';
|
|
541
|
+
|
|
542
|
+
const postFactory = factory()
|
|
543
|
+
.model(postModel)
|
|
544
|
+
.attrs({
|
|
545
|
+
title: () => faker.lorem.sentence(),
|
|
546
|
+
content: () => faker.lorem.paragraphs(),
|
|
547
|
+
})
|
|
548
|
+
.afterCreate((post, schema) => {
|
|
549
|
+
// Automatically assign to first user
|
|
550
|
+
const user = schema.users.first();
|
|
551
|
+
if (user) {
|
|
552
|
+
post.update({ author: user });
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
.build();
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
<details>
|
|
559
|
+
<summary><strong>Key points</strong></summary>
|
|
560
|
+
|
|
561
|
+
- **Build order** — When you call `collection.create(...)` (with optional traits/attrs), the library:
|
|
562
|
+
1. **Factory** evaluates attrs and traits, resolves IDs, returns base attributes (no associations yet);
|
|
563
|
+
2. **Collection** creates the model and **saves it to the DB** so the parent exists;
|
|
564
|
+
3. **Factory** creates or links related models (they can now resolve the parent);
|
|
565
|
+
4. **Collection** reloads the model and applies relationship FK updates (including inverse sync). So the parent is always saved before associations run.
|
|
566
|
+
- **Schema type for associations** — Pass your schema collections type to the factory so the `afterCreate` get full type inference and IDE support. Associations should be 'wired' separately:
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
factory<TestCollections>()
|
|
570
|
+
.model(userModel)
|
|
571
|
+
.associations({
|
|
572
|
+
posts: associations.createMany<PostModel, TestCollections>(
|
|
573
|
+
postModel,
|
|
574
|
+
3,
|
|
575
|
+
'published',
|
|
576
|
+
), // model traits and attributes are suggested by IDE
|
|
577
|
+
})
|
|
578
|
+
.afterCreate((user, schema) => {
|
|
579
|
+
schema.posts.first(); // schema is fully typed
|
|
580
|
+
})
|
|
581
|
+
.build();
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
</details>
|
|
585
|
+
|
|
586
|
+
### 5. Schema
|
|
587
|
+
|
|
588
|
+
The schema is your in-memory database that ties everything together.
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
import { schema } from 'miragejs-orm';
|
|
592
|
+
|
|
593
|
+
const testSchema = schema()
|
|
594
|
+
.collections({
|
|
595
|
+
users: userCollection,
|
|
596
|
+
posts: postCollection,
|
|
597
|
+
comments: commentCollection,
|
|
598
|
+
})
|
|
599
|
+
.build();
|
|
600
|
+
|
|
601
|
+
// Now you can use all collections
|
|
602
|
+
testSchema.users.create({ name: 'Alice' });
|
|
603
|
+
testSchema.posts.all();
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
#### Fixtures
|
|
607
|
+
|
|
608
|
+
Load initial data from fixtures. Fixtures are defined at the **collection level** and support a `strategy` option to control when they're loaded:
|
|
609
|
+
|
|
610
|
+
- `'manual'` (default) - Load fixtures manually by calling `loadFixtures()`
|
|
611
|
+
- `'auto'` - Load fixtures automatically during schema setup
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
import { schema, collection } from 'miragejs-orm';
|
|
615
|
+
|
|
616
|
+
// Manual loading (default)
|
|
617
|
+
const testSchema = schema()
|
|
618
|
+
.collections({
|
|
619
|
+
users: collection()
|
|
620
|
+
.model(userModel)
|
|
621
|
+
.fixtures([
|
|
622
|
+
{ id: '1', name: 'Alice', email: 'alice@example.com' },
|
|
623
|
+
{ id: '2', name: 'Bob', email: 'bob@example.com' },
|
|
624
|
+
])
|
|
625
|
+
.build(),
|
|
626
|
+
})
|
|
627
|
+
.build();
|
|
628
|
+
|
|
629
|
+
// Load fixtures manually when needed
|
|
630
|
+
testSchema.loadFixtures(); // Loads all collection fixtures
|
|
631
|
+
testSchema.users.loadFixtures(); // Or load for specific collection
|
|
632
|
+
|
|
633
|
+
// Automatic loading with strategy option
|
|
634
|
+
const autoSchema = schema()
|
|
635
|
+
.collections({
|
|
636
|
+
users: collection()
|
|
637
|
+
.model(userModel)
|
|
638
|
+
.fixtures(
|
|
639
|
+
[
|
|
640
|
+
{ id: '1', name: 'Alice', email: 'alice@example.com' },
|
|
641
|
+
{ id: '2', name: 'Bob', email: 'bob@example.com' },
|
|
642
|
+
],
|
|
643
|
+
{ strategy: 'auto' }, // Fixtures load automatically during setup
|
|
644
|
+
)
|
|
645
|
+
.build(),
|
|
646
|
+
})
|
|
647
|
+
.build(); // Fixtures are already loaded!
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
#### Seeds
|
|
651
|
+
|
|
652
|
+
Define seed scenarios at the collection level for different testing contexts:
|
|
653
|
+
|
|
654
|
+
```typescript
|
|
655
|
+
import { collection, schema } from 'miragejs-orm';
|
|
656
|
+
import { faker } from '@faker-js/faker';
|
|
657
|
+
|
|
658
|
+
// Define seeds in the collection builder. Use a "default" scenario for loadSeeds({ onlyDefault: true }).
|
|
659
|
+
const userCollection = collection()
|
|
660
|
+
.model(userModel)
|
|
661
|
+
.factory(userFactory)
|
|
662
|
+
.seeds({
|
|
663
|
+
default: (schema) => {
|
|
664
|
+
// Loaded when calling testSchema.loadSeeds({ onlyDefault: true })
|
|
665
|
+
schema.users.create({
|
|
666
|
+
name: 'Demo User',
|
|
667
|
+
email: 'demo@example.com',
|
|
668
|
+
role: 'user',
|
|
669
|
+
});
|
|
670
|
+
},
|
|
671
|
+
userForm: (schema) => {
|
|
672
|
+
// Create a user with all fields populated for form testing
|
|
673
|
+
schema.users.create({
|
|
674
|
+
name: 'John Doe',
|
|
675
|
+
email: 'john.doe@example.com',
|
|
676
|
+
role: 'admin',
|
|
677
|
+
bio: 'Software developer with 10 years of experience',
|
|
678
|
+
avatar: 'https://i.pravatar.cc/150?img=12',
|
|
679
|
+
isActive: true,
|
|
680
|
+
createdAt: new Date('2024-01-15').toISOString(),
|
|
681
|
+
});
|
|
682
|
+
},
|
|
683
|
+
|
|
684
|
+
adminUser: (schema) => {
|
|
685
|
+
// Create admin user for permission testing
|
|
686
|
+
schema.users.create({
|
|
687
|
+
name: 'Admin User',
|
|
688
|
+
email: 'admin@example.com',
|
|
689
|
+
role: 'admin',
|
|
690
|
+
});
|
|
691
|
+
},
|
|
692
|
+
})
|
|
693
|
+
.build();
|
|
694
|
+
|
|
695
|
+
const postCollection = collection()
|
|
696
|
+
.model(postModel)
|
|
697
|
+
.factory(postFactory)
|
|
698
|
+
.seeds({
|
|
699
|
+
postAuthor: (schema) => {
|
|
700
|
+
// Create posts and assign a user to a random subset
|
|
701
|
+
schema.posts.createMany(20);
|
|
702
|
+
const user = schema.users.create({
|
|
703
|
+
name: 'Alice Author',
|
|
704
|
+
email: 'alice@example.com',
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// Assign user to random 5 posts
|
|
708
|
+
const allPosts = schema.posts.all().models;
|
|
709
|
+
const randomPosts = faker.helpers.arrayElements(allPosts, 5);
|
|
710
|
+
|
|
711
|
+
randomPosts.forEach((post) => {
|
|
712
|
+
post.update({ author: user });
|
|
713
|
+
});
|
|
714
|
+
},
|
|
715
|
+
})
|
|
716
|
+
.build();
|
|
717
|
+
|
|
718
|
+
const testSchema = schema()
|
|
719
|
+
.collections({
|
|
720
|
+
users: userCollection,
|
|
721
|
+
posts: postCollection,
|
|
722
|
+
})
|
|
723
|
+
.build();
|
|
724
|
+
|
|
725
|
+
// Load all seeds for all collections
|
|
726
|
+
await testSchema.loadSeeds();
|
|
727
|
+
|
|
728
|
+
// Or load seeds for a specific collection
|
|
729
|
+
await testSchema.users.loadSeeds();
|
|
730
|
+
|
|
731
|
+
// Or load a specific scenario for a collection
|
|
732
|
+
await testSchema.users.loadSeeds('userForm');
|
|
733
|
+
await testSchema.posts.loadSeeds('postAuthor');
|
|
734
|
+
|
|
735
|
+
// Load only default scenarios (e.g. for development mock server)
|
|
736
|
+
await testSchema.loadSeeds({ onlyDefault: true });
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
<details>
|
|
740
|
+
<summary><strong>Key points</strong></summary>
|
|
741
|
+
|
|
742
|
+
- **Create models only through the schema collection API** — Use `testSchema.users.create()`, `testSchema.posts.find()`, etc. Avoid creating or mutating data only via `schema.db`; go through collections so relationships, serializers, and identity managers stay consistent.
|
|
743
|
+
- **Default vs named seeds** — Use the **default** seed scenario for the development environment (e.g. `loadSeeds({ onlyDefault: true })` when starting the mock server). Use **named** seed scenarios for specific test cases (e.g. `loadSeeds('userForm')` or `loadSeeds('postAuthor')`). Combining both gives you a stable dev dataset and targeted test data.
|
|
744
|
+
|
|
745
|
+
</details>
|
|
746
|
+
|
|
747
|
+
### 6. Serializers
|
|
748
|
+
|
|
749
|
+
Serializers control how models are converted to JSON. Configure them per collection via the **Serializer** class or by passing a **SerializerConfig** object. Options can be overridden at call time (e.g. `model.toJSON()` or `model.serializer(options)`).
|
|
750
|
+
|
|
751
|
+
#### Serializer options (SerializerConfig)
|
|
752
|
+
|
|
753
|
+
- **`select`** — Which attributes to include.
|
|
754
|
+
|
|
755
|
+
- **Array:** include only listed keys, e.g. `['id', 'name', 'email']`.
|
|
756
|
+
- **Object:** `{ key: true }` include only those keys; `{ key: false }` exclude those keys (include all others).
|
|
757
|
+
|
|
758
|
+
- **`with`** — Which relationships to include and how.
|
|
759
|
+
|
|
760
|
+
- **Array:** relationship names to include, e.g. `['posts', 'author']`.
|
|
761
|
+
- **Object:** `{ relationName: true }` or `{ relationName: false }` to include/exclude; or `{ relationName: { select: [...], mode: 'embedded' } }` for nested options (e.g. which fields on the relation, or per-relation mode override).
|
|
762
|
+
|
|
763
|
+
- **`relationsMode`** — How to output relationships (default: `'foreignKey'`).
|
|
764
|
+
|
|
765
|
+
- `'foreignKey'` — Only foreign key IDs; no nested relationship data.
|
|
766
|
+
- `'embedded'` — Relationships nested in the model; foreign keys removed.
|
|
767
|
+
- `'embedded+foreignKey'` — Nested and keep foreign keys.
|
|
768
|
+
- `'sideLoaded'` — Relationships at top level (requires `root`). `BelongsTo` as single-item arrays.
|
|
769
|
+
- `'sideLoaded+foreignKey'` — Same and keep foreign keys in attributes.
|
|
770
|
+
- Per-relation overrides are possible inside `with` (e.g. `with: { posts: { mode: 'embedded' } }`).
|
|
771
|
+
|
|
772
|
+
- **`root`** — Wrap the serialized output in a root key.
|
|
773
|
+
- `true` — Use model/collection name (e.g. `{ user: { ... } }`).
|
|
774
|
+
- `false` — No wrapping (default).
|
|
775
|
+
- **String** — Custom key (e.g. `'userData'` → `{ userData: { ... } }`).
|
|
776
|
+
|
|
777
|
+
#### Using the Serializer class
|
|
778
|
+
|
|
779
|
+
```typescript
|
|
780
|
+
import { model, collection, schema, Serializer } from 'miragejs-orm';
|
|
781
|
+
import type { SerializerConfig } from 'miragejs-orm';
|
|
782
|
+
|
|
783
|
+
interface UserAttrs {
|
|
784
|
+
id: string;
|
|
785
|
+
name: string;
|
|
786
|
+
email: string;
|
|
787
|
+
password: string;
|
|
788
|
+
role: string;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
interface UserJSON {
|
|
792
|
+
id: string;
|
|
793
|
+
name: string;
|
|
794
|
+
email: string;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const userModel = model('user', 'users').attrs<UserAttrs>().build();
|
|
798
|
+
|
|
799
|
+
const userSerializer = new Serializer(userModel, {
|
|
800
|
+
select: ['id', 'name', 'email'],
|
|
801
|
+
with: { posts: true },
|
|
802
|
+
relationsMode: 'embedded',
|
|
803
|
+
root: true,
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const testSchema = schema()
|
|
807
|
+
.collections({
|
|
808
|
+
users: collection().model(userModel).serializer(userSerializer).build(),
|
|
809
|
+
})
|
|
810
|
+
.build();
|
|
811
|
+
|
|
812
|
+
const user = testSchema.users.create({
|
|
813
|
+
name: 'Alice',
|
|
814
|
+
email: 'alice@example.com',
|
|
815
|
+
password: 'secret',
|
|
816
|
+
role: 'admin',
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
const json = user.toJSON();
|
|
820
|
+
// With root: true → { user: { id: '1', name: 'Alice', email: 'alice@example.com', posts: [...] } }
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
#### Collection-level serializer config (object)
|
|
824
|
+
|
|
825
|
+
Pass options directly to the collection instead of a Serializer instance. The collection builds an internal serializer from this config.
|
|
826
|
+
|
|
827
|
+
```typescript
|
|
828
|
+
const testSchema = schema()
|
|
829
|
+
.collections({
|
|
830
|
+
users: collection()
|
|
831
|
+
.model(userModel)
|
|
832
|
+
.serializer({
|
|
833
|
+
select: ['id', 'name', 'email'],
|
|
834
|
+
root: 'userData',
|
|
835
|
+
with: ['posts'],
|
|
836
|
+
relationsMode: 'embedded',
|
|
837
|
+
})
|
|
838
|
+
.build(),
|
|
839
|
+
})
|
|
840
|
+
.build();
|
|
841
|
+
|
|
842
|
+
const user = testSchema.users.create({ name: 'Bob', email: 'bob@example.com' });
|
|
843
|
+
console.log(user.toJSON());
|
|
844
|
+
// { userData: { id: '1', name: 'Bob', email: 'bob@example.com', posts: [...] } }
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
#### Method-level overrides
|
|
848
|
+
|
|
849
|
+
Override serializer options at call time using the **model’s** `serialize()` method (or `toJSON()` for default options). The model uses the collection’s serializer and passes through your overrides:
|
|
850
|
+
|
|
851
|
+
```typescript
|
|
852
|
+
// Override options when serializing (uses the collection’s serializer)
|
|
853
|
+
const json = user.serialize({ root: false });
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
<details>
|
|
857
|
+
<summary><strong>Key points (vs original MirageJS)</strong></summary>
|
|
858
|
+
|
|
859
|
+
- **Built-in serialization; use model methods** — Unlike legacy MirageJS, you don’t need to call serializers directly. Pass the serializer (or config) in the collection config and use **model methods only**: `model.toJSON()`, `model.serialize(options)`, and `collection.toJSON()` for lists. Serialization is built into the model and collection.
|
|
860
|
+
- **Config-based API** — Serialization is driven by **select**, **with**, **relationsMode**, and **root** (no separate `attrs` / `embed` / `include`). Relationship inclusion and shape are controlled by `with` and `relationsMode` (e.g. `embedded`, `foreignKey`, `sideLoaded`).
|
|
861
|
+
- **One level of nested relationships** — Only one level of nested `with` is supported when serializing related models. Deeper nesting is not part of the serializer API; keep payloads one level for related data.
|
|
862
|
+
- **Custom reusable serializers** — Use the **Serializer** class to define a named, reusable serializer (with your own options and optional custom subclass). Attach it to a collection or use it in tests so the same output shape is reused across test files and handlers.
|
|
863
|
+
|
|
864
|
+
</details>
|
|
865
|
+
|
|
866
|
+
### 7. Records vs Models
|
|
867
|
+
|
|
868
|
+
Understanding the distinction between **Records** and **Models** is fundamental to working with miragejs-orm:
|
|
869
|
+
|
|
870
|
+
**Records** are plain JavaScript objects stored in the database (`DbCollection`). They contain:
|
|
871
|
+
|
|
872
|
+
- Simple data attributes (name, email, etc.)
|
|
873
|
+
- Foreign keys (userId, postIds, etc.)
|
|
874
|
+
- An `id` field
|
|
875
|
+
- No methods or behavior
|
|
876
|
+
|
|
877
|
+
**Models** are class instances that wrap records and provide rich functionality:
|
|
878
|
+
|
|
879
|
+
- All record attributes via accessors (`user.name`, `post.title`)
|
|
880
|
+
- Relationship accessors (`user.posts`, `post.author`)
|
|
881
|
+
- CRUD methods (`.save()`, `.update()`, `.destroy()`, `.reload()`)
|
|
882
|
+
- Relationship methods (`.related()`, `.link()`, `.unlink()`)
|
|
883
|
+
- Serialization (`.toJSON()`, `.toString()`)
|
|
884
|
+
- Status tracking (`.isNew()`, `.isSaved()`)
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
// When you create a model, it materializes into a Model instance
|
|
888
|
+
const user = testSchema.users.create({
|
|
889
|
+
name: 'Alice',
|
|
890
|
+
email: 'alice@example.com',
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// The Model instance wraps a Record stored in the database
|
|
894
|
+
console.log(user instanceof Model); // true
|
|
895
|
+
console.log(user.name); // 'Alice' - attribute accessor
|
|
896
|
+
console.log(user.posts); // ModelCollection - relationship accessor
|
|
897
|
+
|
|
898
|
+
// Under the hood, the record is just:
|
|
899
|
+
// { id: '1', name: 'Alice', email: 'alice@example.com', postIds: [] }
|
|
900
|
+
|
|
901
|
+
// Models are materialized when:
|
|
902
|
+
// - Creating: testSchema.users.create(...)
|
|
903
|
+
// - Finding: testSchema.users.find('1')
|
|
904
|
+
// - Querying: testSchema.users.findMany({ where: ... })
|
|
905
|
+
// - Accessing relationships: user.posts (returns ModelCollection of Models)
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
**Why This Matters:**
|
|
909
|
+
|
|
910
|
+
- 🗄️ **Storage Efficiency** - The database stores lightweight records, not heavy model instances
|
|
911
|
+
- 🔄 **Fresh Data** - Each query materializes new model instances with the latest record data
|
|
912
|
+
- 🎯 **Type Safety** - Models provide type-safe accessors and methods, records are just data
|
|
913
|
+
- 🔗 **Relationships** - Models handle relationship logic, records only store foreign keys
|
|
914
|
+
|
|
915
|
+
---
|
|
916
|
+
|
|
917
|
+
<div align="center">
|
|
918
|
+
|
|
919
|
+
## 🎯 Usage Examples
|
|
920
|
+
|
|
921
|
+
</div>
|
|
922
|
+
|
|
923
|
+
### With MSW (Mock Service Worker)
|
|
924
|
+
|
|
925
|
+
```typescript
|
|
926
|
+
import { http, HttpResponse } from 'msw';
|
|
927
|
+
import { setupServer } from 'msw/node';
|
|
928
|
+
import { schema, model, collection, factory } from 'miragejs-orm';
|
|
929
|
+
|
|
930
|
+
// Setup your schema
|
|
931
|
+
const testSchema = schema()
|
|
932
|
+
.collections({
|
|
933
|
+
users: collection().model(userModel).factory(userFactory).build(),
|
|
934
|
+
})
|
|
935
|
+
.build();
|
|
936
|
+
|
|
937
|
+
// Seed data
|
|
938
|
+
testSchema.users.createMany(10);
|
|
939
|
+
|
|
940
|
+
// Create MSW handlers
|
|
941
|
+
const handlers = [
|
|
942
|
+
http.get('/api/users', () => {
|
|
943
|
+
const users = testSchema.users.all();
|
|
944
|
+
return HttpResponse.json({ users: users.toJSON() });
|
|
945
|
+
}),
|
|
946
|
+
|
|
947
|
+
http.get('/api/users/:id', ({ params }) => {
|
|
948
|
+
const user = testSchema.users.find(params.id as string);
|
|
949
|
+
if (!user) {
|
|
950
|
+
return new HttpResponse(null, { status: 404 });
|
|
951
|
+
}
|
|
952
|
+
return HttpResponse.json(user.toJSON());
|
|
953
|
+
}),
|
|
954
|
+
|
|
955
|
+
http.post('/api/users', async ({ request }) => {
|
|
956
|
+
const body = await request.json();
|
|
957
|
+
const user = testSchema.users.create(body);
|
|
958
|
+
return HttpResponse.json(user.toJSON(), { status: 201 });
|
|
959
|
+
}),
|
|
960
|
+
];
|
|
961
|
+
|
|
962
|
+
const server = setupServer(...handlers);
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
### In Testing (Jest)
|
|
966
|
+
|
|
967
|
+
With Jest, use a shared schema and reset the database in `beforeEach` so each test starts with a clean state:
|
|
968
|
+
|
|
969
|
+
```typescript
|
|
970
|
+
import { describe, it, expect, beforeEach } from '@jest/globals';
|
|
971
|
+
import { testSchema } from '@test/schema';
|
|
972
|
+
|
|
973
|
+
describe('User Management', () => {
|
|
974
|
+
beforeEach(() => {
|
|
975
|
+
testSchema.db.emptyData();
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it('should create a user with posts', () => {
|
|
979
|
+
const user = testSchema.users.create({
|
|
980
|
+
name: 'Alice',
|
|
981
|
+
email: 'alice@example.com',
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
testSchema.posts.createMany(3, { authorId: user.id });
|
|
985
|
+
|
|
986
|
+
expect(user.posts.length).toBe(3);
|
|
987
|
+
expect(user.posts.models[0].author.id).toBe(user.id);
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it('should handle complex relationships', () => {
|
|
991
|
+
const user1 = testSchema.users.create({ name: 'Alice' });
|
|
992
|
+
const user2 = testSchema.users.create({ name: 'Bob' });
|
|
993
|
+
|
|
994
|
+
const post = testSchema.posts.create({
|
|
995
|
+
title: 'Hello',
|
|
996
|
+
authorId: user1.id,
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
testSchema.comments.create({
|
|
1000
|
+
content: 'Great post!',
|
|
1001
|
+
postId: post.id,
|
|
1002
|
+
userId: user2.id,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
expect(post.comments.length).toBe(1);
|
|
1006
|
+
expect(post.comments.models[0].user.name).toBe('Bob');
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
### In Testing (Vitest Context)
|
|
1012
|
+
|
|
1013
|
+
With Vitest, you can inject the schema as a **fixture** so each test receives a `schema` argument and the DB is cleared after the test automatically. Extend Vitest’s `test` and use the extended `test` from your context:
|
|
1014
|
+
|
|
1015
|
+
```typescript
|
|
1016
|
+
// test/context.ts
|
|
1017
|
+
import { test as baseTest } from 'vitest';
|
|
1018
|
+
import { testSchema } from './schema';
|
|
1019
|
+
|
|
1020
|
+
export const test = baseTest.extend<{ schema: typeof testSchema }>({
|
|
1021
|
+
schema: async ({}, use) => {
|
|
1022
|
+
await use(testSchema);
|
|
1023
|
+
testSchema.db.emptyData(); // Teardown: clean after each test
|
|
1024
|
+
},
|
|
1025
|
+
});
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
```typescript
|
|
1029
|
+
// features/users/api/createUser.test.ts
|
|
1030
|
+
import { test, describe, expect } from '@test/context';
|
|
1031
|
+
|
|
1032
|
+
describe('createUser', () => {
|
|
1033
|
+
test('creates user and returns serialized', async ({ schema }) => {
|
|
1034
|
+
const result = await createUser({
|
|
1035
|
+
name: 'Alice',
|
|
1036
|
+
email: 'alice@example.com',
|
|
1037
|
+
});
|
|
1038
|
+
const user = schema.users.find(result.id)!;
|
|
1039
|
+
|
|
1040
|
+
expect(result).toMatchObject({ name: user.name, email: user.email });
|
|
1041
|
+
expect(user).toBeDefined();
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
test('can create posts for user', async ({ schema }) => {
|
|
1045
|
+
const user = schema.users.create({ name: 'Bob', email: 'bob@example.com' });
|
|
1046
|
+
schema.posts.createMany(2, { authorId: user.id });
|
|
1047
|
+
|
|
1048
|
+
expect(user.posts.length).toBe(2);
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
Each test gets a fresh logical state: the same `testSchema` instance is used, and `emptyData()` runs after the fixture’s `use()` completes.
|
|
1054
|
+
|
|
1055
|
+
### In Storybook
|
|
1056
|
+
|
|
1057
|
+
```typescript
|
|
1058
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
1059
|
+
import { UserList } from './UserList';
|
|
1060
|
+
import { schema, model, collection, factory } from 'miragejs-orm';
|
|
1061
|
+
|
|
1062
|
+
// Setup mock data for stories
|
|
1063
|
+
const setupMockData = (count: number) => {
|
|
1064
|
+
const testSchema = schema()
|
|
1065
|
+
.collections({
|
|
1066
|
+
users: collection()
|
|
1067
|
+
.model(userModel)
|
|
1068
|
+
.factory(userFactory)
|
|
1069
|
+
.build(),
|
|
1070
|
+
})
|
|
1071
|
+
.build();
|
|
1072
|
+
|
|
1073
|
+
return testSchema.users.createMany(count);
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
const meta: Meta<typeof UserList> = {
|
|
1077
|
+
component: UserList,
|
|
1078
|
+
title: 'UserList',
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
export default meta;
|
|
1082
|
+
|
|
1083
|
+
export const Empty: StoryObj<typeof UserList> = {
|
|
1084
|
+
render: () => <UserList users={[]} />,
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
export const WithUsers: StoryObj<typeof UserList> = {
|
|
1088
|
+
render: () => {
|
|
1089
|
+
const users = setupMockData(5);
|
|
1090
|
+
return <UserList users={users.toJSON()} />;
|
|
1091
|
+
},
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
export const WithManyUsers: StoryObj<typeof UserList> = {
|
|
1095
|
+
render: () => {
|
|
1096
|
+
const users = setupMockData(50);
|
|
1097
|
+
return <UserList users={users.toJSON()} />;
|
|
1098
|
+
},
|
|
1099
|
+
};
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
---
|
|
1103
|
+
|
|
1104
|
+
<div align="center">
|
|
1105
|
+
|
|
1106
|
+
## 🔧 Advanced Features
|
|
1107
|
+
|
|
1108
|
+
</div>
|
|
1109
|
+
|
|
1110
|
+
### Custom Identity Managers
|
|
1111
|
+
|
|
1112
|
+
Control how IDs are generated via **IdentityManagerConfig** (no need to define a custom class). You can set a default at the **schema level** and override per **collection**.
|
|
1113
|
+
|
|
1114
|
+
**Config options:** `initialCounter` (required), optional `initialUsedIds`, optional `idGenerator(currentId) => nextId`.
|
|
1115
|
+
|
|
1116
|
+
```typescript
|
|
1117
|
+
import { schema, collection, IdentityManager } from 'miragejs-orm';
|
|
1118
|
+
import type { IdentityManagerConfig } from 'miragejs-orm';
|
|
1119
|
+
|
|
1120
|
+
// Global default: string IDs starting at "1" (schema-level)
|
|
1121
|
+
const testSchema = schema()
|
|
1122
|
+
.identityManager({ initialCounter: '1' })
|
|
1123
|
+
.collections({
|
|
1124
|
+
users: userCollection,
|
|
1125
|
+
posts: postCollection,
|
|
1126
|
+
})
|
|
1127
|
+
.build();
|
|
1128
|
+
|
|
1129
|
+
// Per-collection override: number IDs for posts only
|
|
1130
|
+
const postCollection = collection()
|
|
1131
|
+
.model(postModel)
|
|
1132
|
+
.identityManager({ initialCounter: 1 })
|
|
1133
|
+
.build();
|
|
1134
|
+
|
|
1135
|
+
// Custom id generator (e.g. UUIDs)
|
|
1136
|
+
const uuidConfig: IdentityManagerConfig<string> = {
|
|
1137
|
+
initialCounter: '0',
|
|
1138
|
+
idGenerator: () => crypto.randomUUID(),
|
|
1139
|
+
};
|
|
1140
|
+
const usersCollection = collection()
|
|
1141
|
+
.model(userModel)
|
|
1142
|
+
.identityManager(uuidConfig)
|
|
1143
|
+
.build();
|
|
1144
|
+
|
|
1145
|
+
// You can also pass an IdentityManager instance per collection
|
|
1146
|
+
const customManager = new IdentityManager({ initialCounter: 100 });
|
|
1147
|
+
collection().model(postModel).identityManager(customManager).build();
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
### Custom Serializers
|
|
1151
|
+
|
|
1152
|
+
Extend the **Serializer** class when you need a reusable, named serializer with custom logic. Override `serialize()` or `serializeCollection()` and call `super.serialize()` / `super.serializeCollection()` to use the default behavior, then adjust the result. When the custom serializer is passed to the collection, you can use **`.toJSON()`** on the model or collection instead of calling the serializer directly.
|
|
1153
|
+
|
|
1154
|
+
```typescript
|
|
1155
|
+
import { model, collection, schema, Serializer } from 'miragejs-orm';
|
|
1156
|
+
import type {
|
|
1157
|
+
SerializerConfig,
|
|
1158
|
+
ModelInstance,
|
|
1159
|
+
ModelCollection,
|
|
1160
|
+
} from 'miragejs-orm';
|
|
1161
|
+
|
|
1162
|
+
// Custom serialized types (optional — use .json() so toJSON() is typed)
|
|
1163
|
+
interface UserJSON {
|
|
1164
|
+
id: string;
|
|
1165
|
+
name: string;
|
|
1166
|
+
email: string;
|
|
1167
|
+
role: string;
|
|
1168
|
+
displayName: string;
|
|
1169
|
+
}
|
|
1170
|
+
interface UserListResponse {
|
|
1171
|
+
data: UserJSON[];
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const userModel = model('user', 'users')
|
|
1175
|
+
.attrs<{ id: string; name: string; email: string; role: string }>()
|
|
1176
|
+
.json<UserJSON, UserJSON[]>()
|
|
1177
|
+
.build();
|
|
1178
|
+
|
|
1179
|
+
// Custom serializer: add computed fields; wrap list response in a "data" envelope
|
|
1180
|
+
class UserApiSerializer extends Serializer<typeof userModel, TestCollections> {
|
|
1181
|
+
serialize(
|
|
1182
|
+
model: ModelInstance<typeof userModel, TestCollections>,
|
|
1183
|
+
options?: Partial<SerializerConfig<typeof userModel, TestCollections>>,
|
|
1184
|
+
) {
|
|
1185
|
+
const json = super.serialize(model, options) as Record<string, unknown>;
|
|
1186
|
+
json.displayName = `${model.name} (${model.email})`;
|
|
1187
|
+
return json;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
serializeCollection(
|
|
1191
|
+
collection: ModelCollection<typeof userModel, TestCollections>,
|
|
1192
|
+
options?: Partial<SerializerConfig<typeof userModel, TestCollections>>,
|
|
1193
|
+
) {
|
|
1194
|
+
const json = super.serializeCollection(collection, options) as Record<
|
|
1195
|
+
string,
|
|
1196
|
+
unknown
|
|
1197
|
+
>;
|
|
1198
|
+
const items = json[this.collectionName] as unknown[];
|
|
1199
|
+
return { data: items };
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const userSerializer = new UserApiSerializer(userModel, {
|
|
1204
|
+
select: ['id', 'name', 'email', 'role'],
|
|
1205
|
+
root: true,
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
const testSchema = schema()
|
|
1209
|
+
.collections({
|
|
1210
|
+
users: collection().model(userModel).serializer(userSerializer).build(),
|
|
1211
|
+
})
|
|
1212
|
+
.build();
|
|
1213
|
+
|
|
1214
|
+
const user = testSchema.users.create({
|
|
1215
|
+
name: 'Alice',
|
|
1216
|
+
email: 'alice@example.com',
|
|
1217
|
+
role: 'user',
|
|
1218
|
+
});
|
|
1219
|
+
const users = testSchema.users.all();
|
|
1220
|
+
|
|
1221
|
+
// Option 1: use .toJSON() (uses the collection's serializer)
|
|
1222
|
+
const singleJson: { user: UserJSON } = user.toJSON();
|
|
1223
|
+
// { user: { id: '1', name: 'Alice', email: 'alice@example.com', role: 'user', displayName: 'Alice (alice@example.com)' } }
|
|
1224
|
+
|
|
1225
|
+
const listJson: UserListResponse = users.toJSON() as UserListResponse;
|
|
1226
|
+
// { data: [{ id: '1', name: 'Alice', email: 'alice@example.com', role: 'user', displayName: '...' }, ...] }
|
|
1227
|
+
|
|
1228
|
+
// Option 2: call the serializer directly (e.g. in handlers or when you hold a serializer reference)
|
|
1229
|
+
userSerializer.serialize(user);
|
|
1230
|
+
userSerializer.serializeCollection(users);
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
### Query Methods
|
|
1234
|
+
|
|
1235
|
+
MirageJS ORM provides powerful query capabilities including advanced operators, logical combinations, and pagination — features not available in standard MirageJS.
|
|
1236
|
+
|
|
1237
|
+
#### Basic Queries
|
|
1238
|
+
|
|
1239
|
+
```typescript
|
|
1240
|
+
// Simple equality
|
|
1241
|
+
const admins = testSchema.users.findMany({ where: { role: 'admin' } });
|
|
1242
|
+
|
|
1243
|
+
// Predicate function
|
|
1244
|
+
const recentPosts = testSchema.posts.findMany({
|
|
1245
|
+
where: (post) => new Date(post.createdAt) > new Date('2024-01-01'),
|
|
1246
|
+
});
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
#### Advanced Query Operators
|
|
1250
|
+
|
|
1251
|
+
```typescript
|
|
1252
|
+
// Comparison operators
|
|
1253
|
+
const youngUsers = testSchema.users.findMany({
|
|
1254
|
+
where: { age: { lt: 30 } }, // Less than
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
const adults = testSchema.users.findMany({
|
|
1258
|
+
where: { age: { gte: 18 } }, // Greater than or equal
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
const rangeUsers = testSchema.users.findMany({
|
|
1262
|
+
where: { age: { between: [25, 35] } }, // Between (inclusive)
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
// String operators
|
|
1266
|
+
const gmailUsers = testSchema.users.findMany({
|
|
1267
|
+
where: { email: { like: '%@gmail.com' } }, // SQL-style wildcards
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
const nameSearch = testSchema.users.findMany({
|
|
1271
|
+
where: { name: { ilike: '%john%' } }, // Case-insensitive search
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
const usersStartingWithA = testSchema.users.findMany({
|
|
1275
|
+
where: { name: { startsWith: 'A' } },
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
// Null checks
|
|
1279
|
+
const usersWithoutEmail = testSchema.users.findMany({
|
|
1280
|
+
where: { email: { isNull: true } },
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
// Array operators
|
|
1284
|
+
const admins = testSchema.users.findMany({
|
|
1285
|
+
where: { tags: { contains: 'admin' } }, // Array includes value
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
const multipleRoles = testSchema.users.findMany({
|
|
1289
|
+
where: { tags: { contains: ['admin', 'moderator'] } }, // Array includes all values
|
|
1290
|
+
});
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
**Available Operators:**
|
|
1294
|
+
|
|
1295
|
+
- Equality: `eq`, `ne`, `in`, `nin`, `isNull`
|
|
1296
|
+
- Comparison: `lt`, `lte`, `gt`, `gte`, `between`
|
|
1297
|
+
- String: `like`, `ilike`, `startsWith`, `endsWith`, `contains`
|
|
1298
|
+
- Array: `contains`, `length`
|
|
1299
|
+
|
|
1300
|
+
#### Logical Operators (AND/OR/NOT)
|
|
1301
|
+
|
|
1302
|
+
```typescript
|
|
1303
|
+
// AND - all conditions must match
|
|
1304
|
+
const activeAdmins = testSchema.users.findMany({
|
|
1305
|
+
where: {
|
|
1306
|
+
AND: [{ status: 'active' }, { role: 'admin' }],
|
|
1307
|
+
},
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
// OR - at least one condition must match
|
|
1311
|
+
const flaggedUsers = testSchema.users.findMany({
|
|
1312
|
+
where: {
|
|
1313
|
+
OR: [{ status: 'suspended' }, { age: { lt: 18 } }],
|
|
1314
|
+
},
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
// NOT - negate condition
|
|
1318
|
+
const nonAdmins = testSchema.users.findMany({
|
|
1319
|
+
where: {
|
|
1320
|
+
NOT: { role: 'admin' },
|
|
1321
|
+
},
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// Complex combinations
|
|
1325
|
+
const eligibleUsers = testSchema.users.findMany({
|
|
1326
|
+
where: {
|
|
1327
|
+
AND: [
|
|
1328
|
+
{
|
|
1329
|
+
OR: [{ status: 'active' }, { status: 'pending' }],
|
|
1330
|
+
},
|
|
1331
|
+
{ NOT: { age: { lt: 18 } } },
|
|
1332
|
+
{ email: { like: '%@company.com' } },
|
|
1333
|
+
],
|
|
1334
|
+
},
|
|
1335
|
+
});
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
#### Pagination
|
|
1339
|
+
|
|
1340
|
+
**Offset-based (Standard)**
|
|
1341
|
+
|
|
1342
|
+
```typescript
|
|
1343
|
+
// Page 1: First 10 users
|
|
1344
|
+
const page1 = testSchema.users.findMany({
|
|
1345
|
+
limit: 10,
|
|
1346
|
+
offset: 0,
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// Page 2: Next 10 users
|
|
1350
|
+
const page2 = testSchema.users.findMany({
|
|
1351
|
+
limit: 10,
|
|
1352
|
+
offset: 10,
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
// Combined with filtering and sorting
|
|
1356
|
+
const activeUsersPage2 = testSchema.users.findMany({
|
|
1357
|
+
where: { status: 'active' },
|
|
1358
|
+
orderBy: { createdAt: 'desc' },
|
|
1359
|
+
offset: 20,
|
|
1360
|
+
limit: 10,
|
|
1361
|
+
});
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
**Cursor-based (Keyset) Pagination**
|
|
1365
|
+
|
|
1366
|
+
More efficient for large datasets and prevents inconsistencies when data changes between requests.
|
|
1367
|
+
|
|
1368
|
+
```typescript
|
|
1369
|
+
// First page
|
|
1370
|
+
const firstPage = testSchema.users.findMany({
|
|
1371
|
+
orderBy: { createdAt: 'desc' },
|
|
1372
|
+
limit: 10,
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
// Next page using last item as cursor
|
|
1376
|
+
const lastUser = firstPage[firstPage.length - 1];
|
|
1377
|
+
const nextPage = testSchema.users.findMany({
|
|
1378
|
+
orderBy: { createdAt: 'desc' },
|
|
1379
|
+
cursor: { createdAt: lastUser.createdAt },
|
|
1380
|
+
limit: 10,
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
// Multi-field cursor for unique sorting
|
|
1384
|
+
const page = testSchema.users.findMany({
|
|
1385
|
+
orderBy: [
|
|
1386
|
+
['score', 'desc'],
|
|
1387
|
+
['createdAt', 'asc'],
|
|
1388
|
+
],
|
|
1389
|
+
cursor: { score: 100, createdAt: new Date('2024-01-15') },
|
|
1390
|
+
limit: 20,
|
|
1391
|
+
});
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
#### Combined Operations
|
|
1395
|
+
|
|
1396
|
+
```typescript
|
|
1397
|
+
// Complex query with all features
|
|
1398
|
+
const results = testSchema.users.findMany({
|
|
1399
|
+
where: {
|
|
1400
|
+
AND: [
|
|
1401
|
+
{ status: 'active' },
|
|
1402
|
+
{
|
|
1403
|
+
OR: [{ role: 'admin' }, { tags: { contains: 'premium' } }],
|
|
1404
|
+
},
|
|
1405
|
+
{ age: { gte: 18 } },
|
|
1406
|
+
],
|
|
1407
|
+
},
|
|
1408
|
+
orderBy: [
|
|
1409
|
+
['lastActive', 'desc'],
|
|
1410
|
+
['name', 'asc'],
|
|
1411
|
+
],
|
|
1412
|
+
offset: 20,
|
|
1413
|
+
limit: 10,
|
|
1414
|
+
});
|
|
1415
|
+
```
|
|
1416
|
+
|
|
1417
|
+
### Direct Database Access
|
|
1418
|
+
|
|
1419
|
+
For low-level operations:
|
|
1420
|
+
|
|
1421
|
+
```typescript
|
|
1422
|
+
// Access raw database
|
|
1423
|
+
const rawUsers = testSchema.db.users.all();
|
|
1424
|
+
|
|
1425
|
+
// Batch operations
|
|
1426
|
+
testSchema.db.emptyData(); // Clear all data
|
|
1427
|
+
testSchema.db.users.insert({ id: '1', name: 'Alice' });
|
|
1428
|
+
testSchema.db.users.remove({ id: '1' });
|
|
1429
|
+
```
|
|
1430
|
+
|
|
1431
|
+
### Debugging
|
|
1432
|
+
|
|
1433
|
+
Enable logging to understand what the ORM is doing under the hood. This is invaluable for debugging tests, understanding query behavior, and troubleshooting data issues.
|
|
1434
|
+
|
|
1435
|
+
**Enable Logging:**
|
|
1436
|
+
|
|
1437
|
+
```typescript
|
|
1438
|
+
import { schema, LogLevel } from 'miragejs-orm';
|
|
1439
|
+
|
|
1440
|
+
const testSchema = schema()
|
|
1441
|
+
.collections({
|
|
1442
|
+
users: userCollection,
|
|
1443
|
+
posts: postCollection,
|
|
1444
|
+
})
|
|
1445
|
+
.logging({
|
|
1446
|
+
enabled: true,
|
|
1447
|
+
level: LogLevel.DEBUG, // or 'debug'
|
|
1448
|
+
})
|
|
1449
|
+
.build();
|
|
1450
|
+
```
|
|
1451
|
+
|
|
1452
|
+
**Log Levels:** Use the `LogLevel` enum or string equivalents.
|
|
1453
|
+
|
|
1454
|
+
```typescript
|
|
1455
|
+
import { schema, LogLevel } from 'miragejs-orm';
|
|
1456
|
+
|
|
1457
|
+
// Debug - Most verbose, shows all operations
|
|
1458
|
+
schema().logging({ enabled: true, level: LogLevel.DEBUG });
|
|
1459
|
+
// Output: Schema initialization, collection registration, create/find operations, query details
|
|
1460
|
+
|
|
1461
|
+
// Info - Important operations and results
|
|
1462
|
+
schema().logging({ enabled: true, level: LogLevel.INFO });
|
|
1463
|
+
// Output: Fixtures loaded, seeds loaded, high-level operations
|
|
1464
|
+
|
|
1465
|
+
// Warn - Potential issues and unusual patterns
|
|
1466
|
+
schema().logging({ enabled: true, level: LogLevel.WARN });
|
|
1467
|
+
// Output: Foreign key mismatches, deprecated usage
|
|
1468
|
+
|
|
1469
|
+
// Error - Only failures and validation errors
|
|
1470
|
+
schema().logging({ enabled: true, level: LogLevel.ERROR });
|
|
1471
|
+
// Output: Operation failures, validation errors
|
|
1472
|
+
|
|
1473
|
+
// Silent - No logging (default)
|
|
1474
|
+
schema().logging({ enabled: true, level: LogLevel.SILENT });
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
**Custom Prefix:**
|
|
1478
|
+
|
|
1479
|
+
```typescript
|
|
1480
|
+
import { schema } from 'miragejs-orm';
|
|
1481
|
+
|
|
1482
|
+
const testSchema = schema()
|
|
1483
|
+
.collections({ users: userCollection })
|
|
1484
|
+
.logging({
|
|
1485
|
+
enabled: true,
|
|
1486
|
+
level: 'debug',
|
|
1487
|
+
prefix: '[MyApp Test]', // Custom prefix instead of default '[Mirage]'
|
|
1488
|
+
})
|
|
1489
|
+
.build();
|
|
1490
|
+
|
|
1491
|
+
// Output: [MyApp Test] DEBUG: Schema initialized
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
**What Gets Logged:**
|
|
1495
|
+
|
|
1496
|
+
```typescript
|
|
1497
|
+
import { schema, collection, LogLevel } from 'miragejs-orm';
|
|
1498
|
+
|
|
1499
|
+
const testSchema = schema()
|
|
1500
|
+
.logging({ enabled: true, level: LogLevel.DEBUG })
|
|
1501
|
+
.collections({
|
|
1502
|
+
users: collection()
|
|
1503
|
+
.model(userModel)
|
|
1504
|
+
.factory(userFactory)
|
|
1505
|
+
.fixtures([{ id: '1', name: 'Alice' }])
|
|
1506
|
+
.seeds({ testData: (schema) => schema.users.create({ name: 'Bob' }) })
|
|
1507
|
+
.build(),
|
|
1508
|
+
})
|
|
1509
|
+
.build();
|
|
1510
|
+
|
|
1511
|
+
// Console output:
|
|
1512
|
+
// [Mirage] DEBUG: Registering collections { count: 1, names: ['users'] }
|
|
1513
|
+
// [Mirage] DEBUG: Collection 'users' initialized { modelName: 'user' }
|
|
1514
|
+
// [Mirage] DEBUG: Schema initialized { collections: ['users'] }
|
|
1515
|
+
|
|
1516
|
+
// Load fixtures
|
|
1517
|
+
testSchema.loadFixtures();
|
|
1518
|
+
// [Mirage] INFO: Fixtures loaded successfully for 'users' { count: 1 }
|
|
1519
|
+
|
|
1520
|
+
// Create a record
|
|
1521
|
+
testSchema.users.create({ name: 'Charlie' });
|
|
1522
|
+
// [Mirage] DEBUG: Creating user { collection: 'users' }
|
|
1523
|
+
// [Mirage] DEBUG: Created user with factory { collection: 'users', id: '2' }
|
|
1524
|
+
|
|
1525
|
+
// Query records
|
|
1526
|
+
const users = testSchema.users.findMany({ where: { name: 'Charlie' } });
|
|
1527
|
+
// [Mirage] DEBUG: Query 'users': findMany
|
|
1528
|
+
// [Mirage] DEBUG: Query 'users' returned 1 records
|
|
1529
|
+
|
|
1530
|
+
// Load seeds for a specific scenario
|
|
1531
|
+
testSchema.users.loadSeeds('testData');
|
|
1532
|
+
// [Mirage] INFO: Seeds loaded successfully for 'users' { scenario: 'testData' }
|
|
1533
|
+
```
|
|
1534
|
+
|
|
1535
|
+
**Use Cases:**
|
|
1536
|
+
|
|
1537
|
+
```typescript
|
|
1538
|
+
import { schema, LogLevel } from 'miragejs-orm';
|
|
1539
|
+
|
|
1540
|
+
// Development - See what's happening
|
|
1541
|
+
const devSchema = schema()
|
|
1542
|
+
.collections({ users: userCollection })
|
|
1543
|
+
.logging({ enabled: true, level: LogLevel.INFO })
|
|
1544
|
+
.build();
|
|
1545
|
+
|
|
1546
|
+
// Testing - Debug failing tests
|
|
1547
|
+
const testSchema = schema()
|
|
1548
|
+
.collections({ users: userCollection })
|
|
1549
|
+
.logging({
|
|
1550
|
+
enabled: process.env.DEBUG === 'true', // Enable via env var
|
|
1551
|
+
level: LogLevel.DEBUG,
|
|
1552
|
+
})
|
|
1553
|
+
.build();
|
|
1554
|
+
```
|
|
1555
|
+
|
|
1556
|
+
---
|
|
1557
|
+
|
|
1558
|
+
<div align="center">
|
|
1559
|
+
|
|
1560
|
+
## 💡 TypeScript Best Practices
|
|
1561
|
+
|
|
1562
|
+
</div>
|
|
1563
|
+
|
|
1564
|
+
MirageJS ORM is built with TypeScript-first design. Here are best practices for getting the most out of type safety.
|
|
1565
|
+
|
|
1566
|
+
### Defining Shareable Model Template Types
|
|
1567
|
+
|
|
1568
|
+
Use `typeof` to create reusable model template types that can be shared across your schema:
|
|
1569
|
+
|
|
1570
|
+
```typescript
|
|
1571
|
+
// -- @test/schema/models/user.model.ts --
|
|
1572
|
+
import { model } from 'miragejs-orm';
|
|
1573
|
+
import type { User } from '@domain/users/types';
|
|
1574
|
+
|
|
1575
|
+
// Define user model attributes type
|
|
1576
|
+
export type UserAttrs = { name: string; email: string; role: string };
|
|
1577
|
+
// Define user model output type to be produced during serialization
|
|
1578
|
+
export type UserJSON = { user: User; current?: boolean };
|
|
1579
|
+
|
|
1580
|
+
// Create user model template
|
|
1581
|
+
export const userModel = model('user', 'users')
|
|
1582
|
+
.attrs<UserAttrs>()
|
|
1583
|
+
.json<UserJSON, User[]>()
|
|
1584
|
+
.build();
|
|
1585
|
+
|
|
1586
|
+
// Define a shareable user model type
|
|
1587
|
+
export type UserModel = typeof userModel;
|
|
1588
|
+
```
|
|
1589
|
+
|
|
1590
|
+
```typescript
|
|
1591
|
+
// -- @test/schema/models/post.model.ts --
|
|
1592
|
+
import { model } from 'miragejs-orm';
|
|
1593
|
+
import type { Post } from '@domain/posts/types';
|
|
1594
|
+
|
|
1595
|
+
// Define post attributes type
|
|
1596
|
+
export type PostAttrs = { title: string; content: string; authorId: string };
|
|
1597
|
+
|
|
1598
|
+
// Create post model template
|
|
1599
|
+
export const postModel = model('post', 'posts')
|
|
1600
|
+
.attrs<PostAttrs>()
|
|
1601
|
+
.json<Post, Post[]>() // Use existing Post entity type without transformations
|
|
1602
|
+
.build();
|
|
1603
|
+
|
|
1604
|
+
// Define shareable post model type
|
|
1605
|
+
export type PostModel = typeof postModel;
|
|
1606
|
+
```
|
|
1607
|
+
|
|
1608
|
+
```typescript
|
|
1609
|
+
// -- @test/schema/collections/user.collection.ts --
|
|
1610
|
+
// Use shareable model types in your collections
|
|
1611
|
+
import {
|
|
1612
|
+
userModel,
|
|
1613
|
+
type UserModel,
|
|
1614
|
+
postModel,
|
|
1615
|
+
type PostModel,
|
|
1616
|
+
} from '@test/schema/models';
|
|
1617
|
+
```
|
|
1618
|
+
|
|
1619
|
+
### Typing Schema
|
|
1620
|
+
|
|
1621
|
+
Define explicit schema types for use across your application (e.g. `TestSchema` / `TestCollections` for a test schema):
|
|
1622
|
+
|
|
1623
|
+
```typescript
|
|
1624
|
+
// -- @test/schema/schema.ts or types.ts --
|
|
1625
|
+
import { schema, collection, relations } from 'miragejs-orm';
|
|
1626
|
+
import type {
|
|
1627
|
+
CollectionConfig,
|
|
1628
|
+
HasMany,
|
|
1629
|
+
BelongsTo,
|
|
1630
|
+
SchemaInstance,
|
|
1631
|
+
Factory,
|
|
1632
|
+
} from 'miragejs-orm';
|
|
1633
|
+
import type { UserModel, PostModel } from './models';
|
|
1634
|
+
|
|
1635
|
+
// Define your schema collections type
|
|
1636
|
+
export type TestCollections = {
|
|
1637
|
+
users: CollectionConfig<
|
|
1638
|
+
UserModel,
|
|
1639
|
+
{ posts: HasMany<PostModel> },
|
|
1640
|
+
Factory<UserModel, 'admin' | 'verified', TestCollections>,
|
|
1641
|
+
TestCollections
|
|
1642
|
+
>;
|
|
1643
|
+
posts: CollectionConfig<
|
|
1644
|
+
PostModel,
|
|
1645
|
+
{ author: BelongsTo<UserModel, 'authorId'> },
|
|
1646
|
+
Factory<PostModel, 'published', TestCollections>,
|
|
1647
|
+
TestCollections
|
|
1648
|
+
>;
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
export type TestSchema = SchemaInstance<TestCollections>;
|
|
1652
|
+
```
|
|
1653
|
+
|
|
1654
|
+
### Typing Model Instances
|
|
1655
|
+
|
|
1656
|
+
Use the `ModelInstance` type to properly type materialized model instances with full relationship support:
|
|
1657
|
+
|
|
1658
|
+
```typescript
|
|
1659
|
+
import type { ModelInstance } from 'miragejs-orm';
|
|
1660
|
+
import type { UserModel } from '@test/schema/models';
|
|
1661
|
+
import type { TestCollections } from '@test/schema/types';
|
|
1662
|
+
|
|
1663
|
+
// Type a user model instance
|
|
1664
|
+
type UserInstance = ModelInstance<UserModel, TestCollections>;
|
|
1665
|
+
|
|
1666
|
+
// Usage in functions or variable assignments
|
|
1667
|
+
function processUser(user: UserInstance) {
|
|
1668
|
+
// Full type safety for attributes
|
|
1669
|
+
console.log(user.name); // ✅ string
|
|
1670
|
+
console.log(user.email); // ✅ string
|
|
1671
|
+
console.log(user.role); // ✅ string
|
|
1672
|
+
|
|
1673
|
+
// Full type safety for relationships
|
|
1674
|
+
console.log(user.posts); // ✅ ModelCollection<PostModel>
|
|
1675
|
+
user.posts.forEach((post) => {
|
|
1676
|
+
console.log(post.title); // ✅ Fully typed
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
// Full type safety for methods
|
|
1680
|
+
user.update({ name: 'New Name' }); // ✅ Type-safe attributes
|
|
1681
|
+
user.save(); // ✅ Method available
|
|
1682
|
+
user.destroy(); // ✅ Method available
|
|
1683
|
+
}
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
**How Type Inference Works:**
|
|
1687
|
+
|
|
1688
|
+
The `ModelInstance<TTemplate, TSchema>` type uses the schema to construct the complete model type:
|
|
1689
|
+
|
|
1690
|
+
1. **Attributes** - Extracted from the model template's `attrs` type
|
|
1691
|
+
2. **Relationships** - Looked up from the schema's collection configuration
|
|
1692
|
+
3. **Foreign Keys** - Automatically inferred from relationship definitions
|
|
1693
|
+
4. **Methods** - Inherited from the base `Model` class (`.save()`, `.update()`, `.destroy()`, `.reload()`, `.link()`, `.unlink()`, `.related()`)
|
|
1694
|
+
5. **Accessors** - Both attribute accessors and relationship accessors are fully typed
|
|
1695
|
+
|
|
1696
|
+
### Typing Collections
|
|
1697
|
+
|
|
1698
|
+
Pass schema type to collections for type-safe `schema` usage and data validation (e.g., seeds, fixtures, etc.):
|
|
1699
|
+
|
|
1700
|
+
```typescript
|
|
1701
|
+
// -- @test/schema/collections/user.collection.ts --
|
|
1702
|
+
import { collection, relations } from 'miragejs-orm';
|
|
1703
|
+
import { userModel, postModel } from '@test/schema/models';
|
|
1704
|
+
import type { TestCollections } from '@test/schema/types';
|
|
1705
|
+
|
|
1706
|
+
// Create user collection
|
|
1707
|
+
const userCollection = collection<TestCollections>()
|
|
1708
|
+
.model(userModel)
|
|
1709
|
+
.relationships({
|
|
1710
|
+
posts: relations.hasMany(postModel),
|
|
1711
|
+
})
|
|
1712
|
+
.seeds({
|
|
1713
|
+
testUsers: (schema) => {
|
|
1714
|
+
schema.users.create({ name: 'John', email: 'john@example.com' });
|
|
1715
|
+
schema.users.create({ name: 'Jane', email: 'jane@example.com' });
|
|
1716
|
+
},
|
|
1717
|
+
})
|
|
1718
|
+
.build();
|
|
1719
|
+
```
|
|
1720
|
+
|
|
1721
|
+
### Typing Factories
|
|
1722
|
+
|
|
1723
|
+
Pass schema type to factories for type-safe `associations` and `afterCreate` hooks. Factory type is `Factory<TModel, TTraits, TSchema>` where `TTraits` is the union of trait names:
|
|
1724
|
+
|
|
1725
|
+
```typescript
|
|
1726
|
+
import { factory, associations } from 'miragejs-orm';
|
|
1727
|
+
import { userModel, postModel } from '@test/schema/models';
|
|
1728
|
+
import type { TestCollections } from '@test/schema/types';
|
|
1729
|
+
|
|
1730
|
+
export const postFactory = factory<TestCollections>() // IDE suggests target model relationships and trait names
|
|
1731
|
+
.model(postModel)
|
|
1732
|
+
.attrs({
|
|
1733
|
+
title: () => 'Sample Post',
|
|
1734
|
+
content: () => 'Content here',
|
|
1735
|
+
})
|
|
1736
|
+
.associations({
|
|
1737
|
+
posts: associations.createMany<PostModel, TestCollections>( // IDE suggests related model attributes and trait names
|
|
1738
|
+
postModel,
|
|
1739
|
+
3,
|
|
1740
|
+
'published',
|
|
1741
|
+
),
|
|
1742
|
+
})
|
|
1743
|
+
.afterCreate((post, schema) => {
|
|
1744
|
+
// schema is fully typed! IDE autocomplete works perfectly
|
|
1745
|
+
const user = schema.users.first();
|
|
1746
|
+
if (user) {
|
|
1747
|
+
post.update({ author: user });
|
|
1748
|
+
}
|
|
1749
|
+
})
|
|
1750
|
+
.build();
|
|
1751
|
+
```
|
|
1752
|
+
|
|
1753
|
+
**Key Benefits:**
|
|
1754
|
+
|
|
1755
|
+
- ✅ Full IDE autocomplete and IntelliSense
|
|
1756
|
+
- ✅ Type-safe relationship definitions
|
|
1757
|
+
- ✅ Catch errors at compile time
|
|
1758
|
+
- ✅ Refactor with confidence
|
|
1759
|
+
|
|
1760
|
+
---
|
|
1761
|
+
|
|
1762
|
+
## 📖 API Reference
|
|
1763
|
+
|
|
1764
|
+
**Full Documentation Website Coming Soon!** 🚀
|
|
1765
|
+
|
|
1766
|
+
We're working on a comprehensive documentation website with detailed API references, interactive examples, and guides. Stay tuned!
|
|
1767
|
+
|
|
1768
|
+
In the meantime:
|
|
1769
|
+
|
|
1770
|
+
- **TypeScript Definitions**: See the [TypeScript definitions](./lib/dist/index.d.ts) for complete API signatures
|
|
1771
|
+
- **IDE Autocomplete**: The library is fully typed — your IDE will provide inline documentation and type hints
|
|
1772
|
+
- **This README**: Contains extensive examples covering most use cases
|
|
1773
|
+
|
|
1774
|
+
---
|
|
1775
|
+
|
|
1776
|
+
## 🤝 Contributing
|
|
1777
|
+
|
|
1778
|
+
We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details.
|
|
1779
|
+
|
|
1780
|
+
---
|
|
1781
|
+
|
|
1782
|
+
## 📄 License
|
|
1783
|
+
|
|
1784
|
+
MIT © [MirageJS](LICENSE)
|
|
1785
|
+
|
|
1786
|
+
---
|
|
1787
|
+
|
|
1788
|
+
**Built with ❤️ for frontend developers who want to move fast without breaking things.**
|