Skip to content

Commit d5598f3

Browse files
authored
msw: Replace preCreateExtension() with v.transform() (#12535)
This has significantly better TypeScript support than the previous monkeypatching approach.
1 parent 0c78b41 commit d5598f3

25 files changed

+346
-459
lines changed
Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,37 @@
11
import { Collection } from '@msw/data';
22
import * as v from 'valibot';
33

4-
import { applyDefault } from '../utils/defaults.js';
5-
import { preCreateExtension } from '../utils/pre-create-extension.js';
4+
import * as counters from '../utils/counters.js';
65
import { seededRandom } from '../utils/random.js';
76

8-
const schema = v.object({
9-
id: v.number(),
10-
11-
crateScopes: v.nullable(v.array(v.any())),
12-
createdAt: v.string(),
13-
endpointScopes: v.nullable(v.array(v.any())),
14-
expiredAt: v.nullable(v.string()),
15-
lastUsedAt: v.nullable(v.string()),
16-
name: v.string(),
17-
token: v.string(),
18-
revoked: v.boolean(),
19-
20-
user: v.any(),
21-
});
22-
23-
function preCreate(attrs, counter) {
24-
applyDefault(attrs, 'id', () => counter);
25-
applyDefault(attrs, 'crateScopes', () => null);
26-
applyDefault(attrs, 'createdAt', () => '2017-11-19T17:59:22Z');
27-
applyDefault(attrs, 'endpointScopes', () => null);
28-
applyDefault(attrs, 'expiredAt', () => null);
29-
applyDefault(attrs, 'lastUsedAt', () => null);
30-
applyDefault(attrs, 'name', () => `API Token ${attrs.id}`);
31-
applyDefault(attrs, 'token', () => generateToken(counter));
32-
applyDefault(attrs, 'revoked', () => false);
33-
34-
if (!attrs.user) {
35-
throw new Error('Missing `user` relationship on `api-token`');
36-
}
37-
}
7+
const schema = v.pipe(
8+
v.object({
9+
id: v.optional(v.number()),
10+
11+
crateScopes: v.optional(v.nullable(v.array(v.string())), null),
12+
createdAt: v.optional(v.string(), '2017-11-19T17:59:22Z'),
13+
endpointScopes: v.optional(v.nullable(v.array(v.string())), null),
14+
expiredAt: v.optional(v.nullable(v.string()), null),
15+
lastUsedAt: v.optional(v.nullable(v.string()), null),
16+
name: v.optional(v.string()),
17+
token: v.optional(v.string()),
18+
revoked: v.optional(v.boolean(), false),
19+
20+
user: v.any(),
21+
}),
22+
v.transform(function (input) {
23+
let counter = counters.increment('apiToken');
24+
let id = input.id ?? counter;
25+
let name = input.name ?? `API Token ${id}`;
26+
let token = input.token ?? generateToken(id);
27+
return { ...input, id, name, token };
28+
}),
29+
);
3830

3931
function generateToken(seed) {
4032
return seededRandom(seed).toString().slice(2);
4133
}
4234

43-
const collection = new Collection({
44-
schema,
45-
extensions: [preCreateExtension(preCreate)],
46-
});
35+
const collection = new Collection({ schema });
4736

4837
export default collection;

packages/crates-io-msw/models/api-token.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { db } from '../index.js';
44

55
test('throws if `user` is not set', async ({ expect }) => {
66
await expect(() => db.apiToken.create({})).rejects.toThrowErrorMatchingInlineSnapshot(
7-
`[Error: Missing \`user\` relationship on \`api-token\`]`,
7+
`[Error: Failed to create a new record with initial values: does not match the schema. Please see the schema validation errors above.]`,
88
);
99
});
1010

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,29 @@
11
import { Collection } from '@msw/data';
22
import * as v from 'valibot';
33

4-
import { applyDefault } from '../utils/defaults.js';
5-
import { preCreateExtension } from '../utils/pre-create-extension.js';
4+
import * as counters from '../utils/counters.js';
65
import { dasherize } from '../utils/strings.js';
76

8-
const schema = v.object({
9-
id: v.string(),
7+
const schema = v.pipe(
8+
v.object({
9+
id: v.optional(v.string()),
1010

11-
category: v.string(),
12-
slug: v.string(),
13-
description: v.string(),
14-
created_at: v.string(),
15-
crates_cnt: v.nullable(v.number()),
16-
});
11+
category: v.optional(v.string()),
12+
slug: v.optional(v.string()),
13+
description: v.optional(v.string()),
14+
created_at: v.optional(v.string(), '2010-06-16T21:30:45Z'),
15+
crates_cnt: v.optional(v.nullable(v.number()), null),
16+
}),
17+
v.transform(function (input) {
18+
let counter = counters.increment('category');
19+
let category = input.category ?? `Category ${counter}`;
20+
let slug = input.slug ?? dasherize(category);
21+
let id = input.id ?? slug;
22+
let description = input.description ?? `This is the description for the category called "${category}"`;
23+
return { ...input, id, category, slug, description };
24+
}),
25+
);
1726

18-
function preCreate(attrs, counter) {
19-
applyDefault(attrs, 'category', () => `Category ${counter}`);
20-
applyDefault(attrs, 'slug', () => dasherize(attrs.category));
21-
applyDefault(attrs, 'id', () => attrs.slug);
22-
applyDefault(attrs, 'description', () => `This is the description for the category called "${attrs.category}"`);
23-
applyDefault(attrs, 'created_at', () => '2010-06-16T21:30:45Z');
24-
applyDefault(attrs, 'crates_cnt', () => null);
25-
}
26-
27-
const collection = new Collection({
28-
schema,
29-
extensions: [preCreateExtension(preCreate)],
30-
});
27+
const collection = new Collection({ schema });
3128

3229
export default collection;
Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,28 @@
11
import { Collection } from '@msw/data';
22
import * as v from 'valibot';
33

4-
import { applyDefault } from '../utils/defaults.js';
5-
import { preCreateExtension } from '../utils/pre-create-extension.js';
6-
7-
const schema = v.object({
8-
id: v.number(),
9-
10-
createdAt: v.string(),
11-
expiresAt: v.string(),
12-
token: v.string(),
13-
14-
crate: v.any(),
15-
invitee: v.any(),
16-
inviter: v.any(),
17-
});
18-
19-
function preCreate(attrs, counter) {
20-
applyDefault(attrs, 'id', () => counter);
21-
applyDefault(attrs, 'createdAt', () => '2016-12-24T12:34:56Z');
22-
applyDefault(attrs, 'expiresAt', () => '2017-01-24T12:34:56Z');
23-
applyDefault(attrs, 'token', () => `secret-token-${attrs.id}`);
24-
25-
if (!attrs.crate) {
26-
throw new Error(`Missing \`crate\` relationship on \`crate-owner-invitation\``);
27-
}
28-
if (!attrs.invitee) {
29-
throw new Error(`Missing \`invitee\` relationship on \`crate-owner-invitation\``);
30-
}
31-
if (!attrs.inviter) {
32-
throw new Error(`Missing \`inviter\` relationship on \`crate-owner-invitation\``);
33-
}
34-
}
35-
36-
const collection = new Collection({
37-
schema,
38-
extensions: [preCreateExtension(preCreate)],
39-
});
4+
import * as counters from '../utils/counters.js';
5+
6+
const schema = v.pipe(
7+
v.object({
8+
id: v.optional(v.number()),
9+
10+
createdAt: v.optional(v.string(), '2016-12-24T12:34:56Z'),
11+
expiresAt: v.optional(v.string(), '2017-01-24T12:34:56Z'),
12+
token: v.optional(v.string()),
13+
14+
crate: v.any(),
15+
invitee: v.any(),
16+
inviter: v.any(),
17+
}),
18+
v.transform(function (input) {
19+
let counter = counters.increment('crateOwnerInvitation');
20+
let id = input.id ?? counter;
21+
let token = input.token ?? `secret-token-${id}`;
22+
return { ...input, id, token };
23+
}),
24+
);
25+
26+
const collection = new Collection({ schema });
4027

4128
export default collection;

packages/crates-io-msw/models/crate-owner-invitation.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@ test('throws if `crate` is not set', async ({ expect }) => {
66
let inviter = await db.user.create({});
77
let invitee = await db.user.create({});
88
await expect(() => db.crateOwnerInvitation.create({ inviter, invitee })).rejects.toThrowErrorMatchingInlineSnapshot(
9-
`[Error: Missing \`crate\` relationship on \`crate-owner-invitation\`]`,
9+
`[Error: Failed to create a new record with initial values: does not match the schema. Please see the schema validation errors above.]`,
1010
);
1111
});
1212

1313
test('throws if `inviter` is not set', async ({ expect }) => {
1414
let crate = await db.crate.create({});
1515
let invitee = await db.user.create({});
1616
await expect(() => db.crateOwnerInvitation.create({ crate, invitee })).rejects.toThrowErrorMatchingInlineSnapshot(
17-
`[Error: Missing \`inviter\` relationship on \`crate-owner-invitation\`]`,
17+
`[Error: Failed to create a new record with initial values: does not match the schema. Please see the schema validation errors above.]`,
1818
);
1919
});
2020

2121
test('throws if `invitee` is not set', async ({ expect }) => {
2222
let crate = await db.crate.create({});
2323
let inviter = await db.user.create({});
2424
await expect(() => db.crateOwnerInvitation.create({ crate, inviter })).rejects.toThrowErrorMatchingInlineSnapshot(
25-
`[Error: Missing \`invitee\` relationship on \`crate-owner-invitation\`]`,
25+
`[Error: Failed to create a new record with initial values: does not match the schema. Please see the schema validation errors above.]`,
2626
);
2727
});
2828

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,28 @@
11
import { Collection } from '@msw/data';
22
import * as v from 'valibot';
33

4-
import { applyDefault } from '../utils/defaults.js';
5-
import { preCreateExtension } from '../utils/pre-create-extension.js';
6-
7-
const schema = v.object({
8-
id: v.number(),
9-
10-
emailNotifications: v.boolean(),
11-
12-
crate: v.any(),
13-
team: v.any(),
14-
user: v.any(),
15-
});
16-
17-
function preCreate(attrs, counter) {
18-
applyDefault(attrs, 'id', () => counter);
19-
applyDefault(attrs, 'emailNotifications', () => true);
20-
applyDefault(attrs, 'team', () => null);
21-
applyDefault(attrs, 'user', () => null);
22-
23-
if (!attrs.crate) {
24-
throw new Error('Missing `crate` relationship on `crate-ownership`');
25-
}
26-
if (!attrs.team && !attrs.user) {
27-
throw new Error('Missing `team` or `user` relationship on `crate-ownership`');
28-
}
29-
if (attrs.team && attrs.user) {
30-
throw new Error('`team` and `user` on a `crate-ownership` are mutually exclusive');
31-
}
32-
}
33-
34-
const collection = new Collection({
35-
schema,
36-
extensions: [preCreateExtension(preCreate)],
37-
});
4+
import * as counters from '../utils/counters.js';
5+
6+
const schema = v.pipe(
7+
v.object({
8+
id: v.optional(v.number()),
9+
10+
emailNotifications: v.optional(v.boolean(), true),
11+
12+
crate: v.any(),
13+
team: v.optional(v.nullable(v.any()), null),
14+
user: v.optional(v.nullable(v.any()), null),
15+
}),
16+
v.transform(function (input) {
17+
let counter = counters.increment('crateOwnership');
18+
let id = input.id ?? counter;
19+
return { ...input, id };
20+
}),
21+
v.check(input => input.crate != null, 'Missing `crate` relationship on `crate-ownership`'),
22+
v.check(input => input.team != null || input.user != null, 'Missing `team` or `user` relationship on `crate-ownership`'),
23+
v.check(input => !(input.team != null && input.user != null), '`team` and `user` on a `crate-ownership` are mutually exclusive'),
24+
);
25+
26+
const collection = new Collection({ schema });
3827

3928
export default collection;

packages/crates-io-msw/models/crate-ownership.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import { db } from '../index.js';
55
test('throws if `crate` is not set', async ({ expect }) => {
66
let user = await db.user.create({});
77
await expect(() => db.crateOwnership.create({ user })).rejects.toThrowErrorMatchingInlineSnapshot(
8-
`[Error: Missing \`crate\` relationship on \`crate-ownership\`]`,
8+
`[Error: Failed to create a new record with initial values: does not match the schema. Please see the schema validation errors above.]`,
99
);
1010
});
1111

1212
test('throws if `team` and `user` are not set', async ({ expect }) => {
1313
let crate = await db.crate.create({});
1414
await expect(() => db.crateOwnership.create({ crate })).rejects.toThrowErrorMatchingInlineSnapshot(
15-
`[Error: Missing \`team\` or \`user\` relationship on \`crate-ownership\`]`,
15+
`[Error: Failed to create a new record with initial values: does not match the schema. Please see the schema validation errors above.]`,
1616
);
1717
});
1818

@@ -21,7 +21,7 @@ test('throws if `team` and `user` are both set', async ({ expect }) => {
2121
let team = await db.team.create({});
2222
let user = await db.user.create({});
2323
await expect(() => db.crateOwnership.create({ crate, team, user })).rejects.toThrowErrorMatchingInlineSnapshot(
24-
`[Error: \`team\` and \`user\` on a \`crate-ownership\` are mutually exclusive]`,
24+
`[Error: Failed to create a new record with initial values: does not match the schema. Please see the schema validation errors above.]`,
2525
);
2626
});
2727

Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,40 @@
11
import { Collection } from '@msw/data';
22
import * as v from 'valibot';
33

4-
import { applyDefault } from '../utils/defaults.js';
5-
import { preCreateExtension } from '../utils/pre-create-extension.js';
4+
import * as counters from '../utils/counters.js';
65

7-
const schema = v.object({
8-
// `v.string()` is used to support some of our old fixtures that use strings here for some reason
9-
id: v.union([v.number(), v.string()]),
6+
const schema = v.pipe(
7+
v.object({
8+
// `v.string()` is used to support some of our old fixtures that use strings here for some reason
9+
id: v.optional(v.union([v.number(), v.string()])),
1010

11-
name: v.string(),
12-
description: v.string(),
13-
downloads: v.number(),
14-
recent_downloads: v.number(),
15-
documentation: v.nullable(v.string()),
16-
homepage: v.nullable(v.string()),
17-
repository: v.nullable(v.string()),
18-
created_at: v.string(),
19-
updated_at: v.string(),
20-
badges: v.array(v.any()),
21-
_extra_downloads: v.array(v.any()),
22-
trustpubOnly: v.boolean(),
11+
name: v.optional(v.string()),
12+
description: v.optional(v.string()),
13+
downloads: v.optional(v.number()),
14+
recent_downloads: v.optional(v.number()),
15+
documentation: v.optional(v.nullable(v.string()), null),
16+
homepage: v.optional(v.nullable(v.string()), null),
17+
repository: v.optional(v.nullable(v.string()), null),
18+
created_at: v.optional(v.string(), '2010-06-16T21:30:45Z'),
19+
updated_at: v.optional(v.string(), '2017-02-24T12:34:56Z'),
20+
badges: v.optional(v.array(v.any()), []),
21+
_extra_downloads: v.optional(v.array(v.any()), []),
22+
trustpubOnly: v.optional(v.boolean(), false),
2323

24-
categories: v.optional(v.array(v.any()), () => []),
25-
keywords: v.optional(v.array(v.any()), () => []),
26-
});
24+
categories: v.optional(v.array(v.any()), []),
25+
keywords: v.optional(v.array(v.any()), []),
26+
}),
27+
v.transform(function (input) {
28+
let counter = counters.increment('crate');
29+
let id = input.id ?? counter;
30+
let name = input.name ?? `crate-${id}`;
31+
let description = input.description ?? `This is the description for the crate called "${name}"`;
32+
let downloads = input.downloads ?? (((id + 13) * 42) % 13) * 12_345;
33+
let recent_downloads = input.recent_downloads ?? (((id + 7) * 31) % 13) * 321;
34+
return { ...input, id, name, description, downloads, recent_downloads };
35+
}),
36+
);
2737

28-
function preCreate(attrs, counter) {
29-
applyDefault(attrs, 'id', () => counter);
30-
applyDefault(attrs, 'name', () => `crate-${attrs.id}`);
31-
applyDefault(attrs, 'description', () => `This is the description for the crate called "${attrs.name}"`);
32-
applyDefault(attrs, 'downloads', () => (((attrs.id + 13) * 42) % 13) * 12_345);
33-
applyDefault(attrs, 'recent_downloads', () => (((attrs.id + 7) * 31) % 13) * 321);
34-
applyDefault(attrs, 'documentation', () => null);
35-
applyDefault(attrs, 'homepage', () => null);
36-
applyDefault(attrs, 'repository', () => null);
37-
applyDefault(attrs, 'created_at', () => '2010-06-16T21:30:45Z');
38-
applyDefault(attrs, 'updated_at', () => '2017-02-24T12:34:56Z');
39-
applyDefault(attrs, 'badges', () => []);
40-
applyDefault(attrs, '_extra_downloads', () => []);
41-
applyDefault(attrs, 'trustpubOnly', () => false);
42-
}
43-
44-
const collection = new Collection({
45-
schema,
46-
extensions: [preCreateExtension(preCreate)],
47-
});
38+
const collection = new Collection({ schema });
4839

4940
export default collection;

0 commit comments

Comments
 (0)