Skip to content

Commit c59b402

Browse files
authored
Merge pull request #109 from gadget-inc/accepts-undefined
Add support for acceptsUndefined: false to safe references
2 parents 1a3b837 + 5488410 commit c59b402

File tree

6 files changed

+269
-100
lines changed

6 files changed

+269
-100
lines changed

package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@gadgetinc/mobx-quick-tree",
3-
"version": "0.7.7",
3+
"version": "0.7.8",
44
"description": "A mirror of mobx-state-tree's API to construct fast, read-only instances that share all the same views",
55
"source": "src/index.ts",
66
"main": "dist/src/index.js",
@@ -65,8 +65,5 @@
6565
"ts-node": "^10.9.2",
6666
"typescript": "^5.4.5",
6767
"yargs": "^17.7.2"
68-
},
69-
"volta": {
70-
"node": "18.12.1"
7168
}
7269
}

spec/reference.spec.ts

Lines changed: 233 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { IAnyClassModelType, Instance, SnapshotOrInstance } from "../src";
44
import { ClassModel, register, types } from "../src";
55
import { TestClassModel } from "./fixtures/TestClassModel";
66
import { TestModel, TestModelSnapshot } from "./fixtures/TestModel";
7+
import { create } from "./helpers";
78

89
const Referrable = types.model("Referenced", {
910
key: types.identifier,
@@ -32,107 +33,256 @@ const Root = types.model("Reference Model", {
3233
});
3334

3435
describe("references", () => {
35-
test("can resolve valid references", () => {
36-
const root = Root.createReadOnly({
37-
model: {
38-
ref: "item-a",
39-
},
40-
refs: [
41-
{ key: "item-a", count: 12 },
42-
{ key: "item-b", count: 523 },
43-
],
36+
describe.each([
37+
["read-only", true],
38+
["observable", false],
39+
])("%s", (_name, readOnly) => {
40+
test("can resolve valid references", () => {
41+
const root = create(
42+
Root,
43+
{
44+
model: {
45+
ref: "item-a",
46+
},
47+
refs: [
48+
{ key: "item-a", count: 12 },
49+
{ key: "item-b", count: 523 },
50+
],
51+
},
52+
readOnly,
53+
);
54+
55+
expect(root.model.ref).toEqual(
56+
expect.objectContaining({
57+
key: "item-a",
58+
count: 12,
59+
}),
60+
);
4461
});
4562

46-
expect(root.model.ref).toEqual(
47-
expect.objectContaining({
48-
key: "item-a",
49-
count: 12,
50-
}),
51-
);
52-
});
63+
test("can resolve valid safe references", () => {
64+
const root = create(
65+
Root,
66+
{
67+
model: {
68+
ref: "item-a",
69+
safeRef: "item-b",
70+
},
71+
refs: [
72+
{ key: "item-a", count: 12 },
73+
{ key: "item-b", count: 523 },
74+
],
75+
},
76+
readOnly,
77+
);
78+
79+
expect(root.model.safeRef).toEqual(
80+
expect.objectContaining({
81+
key: "item-b",
82+
count: 523,
83+
}),
84+
);
85+
});
5386

54-
test("throws for invalid refs", () => {
55-
const createRoot = () =>
56-
Root.createReadOnly({
57-
model: {
58-
ref: "item-c",
87+
test("does not throw for invalid safe references", () => {
88+
const root = create(
89+
Root,
90+
{
91+
model: {
92+
ref: "item-a",
93+
safeRef: "item-c",
94+
},
95+
refs: [
96+
{ key: "item-a", count: 12 },
97+
{ key: "item-b", count: 523 },
98+
],
5999
},
60-
refs: [
61-
{ key: "item-a", count: 12 },
62-
{ key: "item-b", count: 523 },
63-
],
100+
readOnly,
101+
);
102+
103+
expect(root.model.safeRef).toBeUndefined();
104+
});
105+
106+
test("safe references marked with allowUndefined false are non-nullable in types-style arrays", () => {
107+
const Referencer = types.model("Referencer", {
108+
safeRefs: types.array(types.safeReference(Referrable, { acceptsUndefined: false })),
64109
});
65110

66-
expect(createRoot).toThrow();
67-
});
111+
const Root = types.model("Reference Model", {
112+
refs: types.array(Referrable),
113+
model: Referencer,
114+
});
115+
const root = create(
116+
Root,
117+
{
118+
refs: [
119+
{ key: "item-a", count: 12 },
120+
{ key: "item-b", count: 523 },
121+
],
122+
model: {
123+
safeRefs: ["item-a", "item-c"],
124+
},
125+
},
126+
readOnly,
127+
);
68128

69-
test("can resolve valid safe references", () => {
70-
const root = Root.createReadOnly({
71-
model: {
72-
ref: "item-a",
73-
safeRef: "item-b",
74-
},
75-
refs: [
76-
{ key: "item-a", count: 12 },
77-
{ key: "item-b", count: 523 },
78-
],
129+
expect(root.model.safeRefs.map((obj) => obj.key)).toEqual(["item-a"]);
130+
131+
type instanceType = (typeof root.model.safeRefs)[0];
132+
assert<Has<instanceType, undefined>>(false);
133+
assert<Has<instanceType, null>>(false);
79134
});
80135

81-
expect(root.model.safeRef).toEqual(
82-
expect.objectContaining({
83-
key: "item-b",
84-
count: 523,
85-
}),
86-
);
87-
});
136+
test("safe references marked with allowUndefined false are non-nullable in types-style maps", () => {
137+
const Referencer = types.model("Referencer", {
138+
safeRefs: types.map(types.safeReference(Referrable, { acceptsUndefined: false })),
139+
});
140+
141+
const Root = types.model("Reference Model", {
142+
refs: types.array(Referrable),
143+
model: Referencer,
144+
});
145+
const root = create(
146+
Root,
147+
{
148+
refs: [
149+
{ key: "item-a", count: 12 },
150+
{ key: "item-b", count: 523 },
151+
],
152+
model: {
153+
safeRefs: {
154+
"item-a": "item-a",
155+
"item-c": "item-c",
156+
},
157+
},
158+
},
159+
readOnly,
160+
);
88161

89-
test("does not throw for invalid safe references", () => {
90-
const root = Root.createReadOnly({
91-
model: {
92-
ref: "item-a",
93-
safeRef: "item-c",
94-
},
95-
refs: [
96-
{ key: "item-a", count: 12 },
97-
{ key: "item-b", count: 523 },
98-
],
162+
expect([...root.model.safeRefs.keys()]).toEqual(["item-a"]);
99163
});
100164

101-
expect(root.model.safeRef).toBeUndefined();
102-
});
165+
test("safe references marked with allowUndefined false are non-nullable in class model arrays", () => {
166+
@register
167+
class Referencer extends ClassModel({
168+
safeRefs: types.array(types.safeReference(Referrable, { acceptsUndefined: false })),
169+
}) {}
170+
171+
@register
172+
class Root extends ClassModel({
173+
refs: types.array(Referrable),
174+
model: Referencer,
175+
}) {}
103176

104-
test("references are equal to the instances they refer to", () => {
105-
const root = Root.createReadOnly({
106-
model: {
107-
ref: "item-a",
108-
safeRef: "item-b",
109-
},
110-
refs: [
111-
{ key: "item-a", count: 12 },
112-
{ key: "item-b", count: 523 },
113-
],
177+
const root = create(
178+
Root,
179+
{
180+
refs: [
181+
{ key: "item-a", count: 12 },
182+
{ key: "item-b", count: 523 },
183+
],
184+
model: {
185+
safeRefs: ["item-a", "item-c"],
186+
},
187+
},
188+
readOnly,
189+
);
190+
191+
expect(root.model.safeRefs.map((obj) => obj.key)).toEqual(["item-a"]);
192+
193+
type instanceType = (typeof root.model.safeRefs)[0];
194+
assert<Has<instanceType, undefined>>(false);
195+
assert<Has<instanceType, null>>(false);
114196
});
115197

116-
expect(root.model.ref).toBe(root.refs[0]);
117-
expect(root.model.ref).toEqual(root.refs[0]);
118-
expect(root.model.ref).toStrictEqual(root.refs[0]);
119-
});
198+
test("safe references marked with allowUndefined false are non-nullable in class model maps", () => {
199+
@register
200+
class Referencer extends ClassModel({
201+
safeRefs: types.map(types.safeReference(Referrable, { acceptsUndefined: false })),
202+
}) {}
203+
204+
@register
205+
class Root extends ClassModel({
206+
refs: types.array(Referrable),
207+
model: Referencer,
208+
}) {}
120209

121-
test("safe references are equal to the instances they refer to", () => {
122-
const root = Root.createReadOnly({
123-
model: {
124-
ref: "item-a",
125-
safeRef: "item-b",
126-
},
127-
refs: [
128-
{ key: "item-a", count: 12 },
129-
{ key: "item-b", count: 523 },
130-
],
210+
const root = create(
211+
Root,
212+
{
213+
refs: [
214+
{ key: "item-a", count: 12 },
215+
{ key: "item-b", count: 523 },
216+
],
217+
model: {
218+
safeRefs: {
219+
"item-a": "item-a",
220+
"item-c": "item-c",
221+
},
222+
},
223+
},
224+
readOnly,
225+
);
226+
227+
expect([...root.model.safeRefs.keys()]).toEqual(["item-a"]);
228+
});
229+
230+
test("references are equal to the instances they refer to", () => {
231+
const root = create(
232+
Root,
233+
{
234+
model: {
235+
ref: "item-a",
236+
safeRef: "item-b",
237+
},
238+
refs: [
239+
{ key: "item-a", count: 12 },
240+
{ key: "item-b", count: 523 },
241+
],
242+
},
243+
readOnly,
244+
);
245+
246+
expect(root.model.ref).toBe(root.refs[0]);
247+
expect(root.model.ref).toEqual(root.refs[0]);
248+
expect(root.model.ref).toStrictEqual(root.refs[0]);
131249
});
132250

133-
expect(root.model.safeRef).toBe(root.refs[1]);
134-
expect(root.model.safeRef).toEqual(root.refs[1]);
135-
expect(root.model.safeRef).toStrictEqual(root.refs[1]);
251+
test("safe references are equal to the instances they refer to", () => {
252+
const root = create(
253+
Root,
254+
{
255+
model: {
256+
ref: "item-a",
257+
safeRef: "item-b",
258+
},
259+
refs: [
260+
{ key: "item-a", count: 12 },
261+
{ key: "item-b", count: 523 },
262+
],
263+
},
264+
readOnly,
265+
);
266+
267+
expect(root.model.safeRef).toBe(root.refs[1]);
268+
expect(root.model.safeRef).toEqual(root.refs[1]);
269+
expect(root.model.safeRef).toStrictEqual(root.refs[1]);
270+
});
271+
});
272+
273+
test("throws for invalid refs", () => {
274+
const createRoot = () =>
275+
Root.createReadOnly({
276+
model: {
277+
ref: "item-c",
278+
},
279+
refs: [
280+
{ key: "item-a", count: 12 },
281+
{ key: "item-b", count: 523 },
282+
],
283+
});
284+
285+
expect(createRoot).toThrow();
136286
});
137287

138288
test("instances of a model reference are assignable to instances of the model", () => {

src/array.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ensureRegistered } from "./class-model";
44
import { getSnapshot } from "./snapshot";
55
import { $context, $parent, $readOnly, $type } from "./symbols";
66
import type { IAnyStateTreeNode, IAnyType, IArrayType, IMSTArray, IStateTreeNode, Instance, TreeContext } from "./types";
7+
import { SafeReferenceType } from "./reference";
78

89
export class QuickArray<T extends IAnyType> extends Array<Instance<T>> implements IMSTArray<T> {
910
static get [Symbol.species]() {
@@ -82,8 +83,13 @@ export class ArrayType<T extends IAnyType> extends BaseType<Array<T["InputType"]
8283
instantiate(snapshot: this["InputType"] | undefined, context: TreeContext, parent: IStateTreeNode | null): this["InstanceType"] {
8384
const array = new QuickArray<T>(this, parent, context);
8485
if (snapshot) {
85-
array.push(...snapshot.map((element) => this.childrenType.instantiate(element, context, array)));
86+
let instances = snapshot.map((element) => this.childrenType.instantiate(element, context, array));
87+
if (this.childrenType instanceof SafeReferenceType && this.childrenType.options?.acceptsUndefined === false) {
88+
instances = instances.filter((instance) => instance != null);
89+
}
90+
array.push(...instances);
8691
}
92+
8793
return array as this["InstanceType"];
8894
}
8995

0 commit comments

Comments
 (0)