Skip to content

Commit 89e6e1a

Browse files
fix(overmind): properly do rehydrate with classes
1 parent e780a1a commit 89e6e1a

File tree

8 files changed

+220
-109
lines changed

8 files changed

+220
-109
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"compilerOptions": {
3-
"target": "es5",
4-
"module": "es2015",
3+
"target": "es6",
4+
"module": "commonjs",
55
"rootDir": "src",
66
"outDir": "dist/",
77
"moduleResolution": "node",
@@ -10,7 +10,7 @@
1010
"inlineSourceMap": true,
1111
"inlineSources": true,
1212
"declaration": true,
13-
"lib": ["es2015", "dom"]
13+
"lib": ["es2017", "dom"]
1414
},
1515
"exclude": ["node_modules", "dist", "es", "lib", "src/**/*.test.ts"]
1616
}

packages/node_modules/overmind/src/Devtools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SERIALIZE } from './utils'
1+
import { SERIALIZE } from './rehydrate'
22

33
export type Message = {
44
type: string

packages/node_modules/overmind/src/index.test.ts

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ function toJSON(obj) {
66
}
77

88
class StateValue {
9-
[SERIALIZE]
109
value = 'foo'
1110
toJSON() {
1211
return {
12+
[SERIALIZE]: true,
1313
value: this.value
1414
}
1515
}
@@ -379,32 +379,6 @@ describe('Overmind', () => {
379379
).not.toThrow()
380380
expect(app.state.item.isAwesome).toBe(false)
381381
})
382-
test.only('should allow rehydration', () => {
383-
expect.assertions(4)
384-
const app = createDefaultOvermind()
385-
const mutation = {
386-
method: 'set',
387-
path: 'foo',
388-
args: ['bar2']
389-
}
390-
app.actions.rehydrateAction([mutation])
391-
expect(app.state.foo).toBe('bar2')
392-
app.actions.rehydrateAction({
393-
item: {
394-
isAwesome: false
395-
},
396-
value: {
397-
value: 'bar'
398-
}
399-
})
400-
expect(app.state.foo).toEqual('bar2')
401-
expect(app.state.item).toEqual({
402-
isAwesome: false
403-
})
404-
expect(app.state.value.toJSON()).toEqual({
405-
value: 'bar'
406-
})
407-
})
408382
})
409383

410384
describe('Overmind mock', () => {

packages/node_modules/overmind/src/index.ts

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,9 @@ import {
2121
Execution,
2222
NestedPartial,
2323
Options,
24-
RehydrateClasses,
2524
ResolveActions,
2625
ResolveState,
2726
SSRMode,
28-
Serializable,
2927
TestMode
3028
} from './internalTypes'
3129
import {
@@ -62,14 +60,15 @@ import {
6260
isPromise,
6361
mergeState,
6462
processState,
65-
rehydrateState
6663
} from './utils'
6764

6865
export * from './types'
6966

70-
export { createOperator, createMutationOperator, ResolveState, ResolveActions, Serializable, }
67+
export { createOperator, createMutationOperator, ResolveState, ResolveActions }
7168

72-
export { MODE_DEFAULT, MODE_TEST, MODE_SSR, SERIALIZE } from './utils'
69+
export { MODE_DEFAULT, MODE_TEST, MODE_SSR } from './utils'
70+
71+
export { SERIALIZE, rehydrate } from './rehydrate'
7372

7473
/** This type can be overwriten by app developers if they want to avoid
7574
* typing and then they can import `Action`, `Operation` etc. directly from
@@ -94,36 +93,6 @@ export interface Reaction extends IReaction<Config> {}
9493

9594
export { json } from './utils'
9695

97-
export const rehydrate = <T extends IState>(state: T, source: IMutation[] | IState, classes: RehydrateClasses<T> = {} as RehydrateClasses<T>) => {
98-
if (Array.isArray(source)) {
99-
const mutations = source as IMutation[]
100-
mutations.forEach((mutation) => {
101-
const pathArray = mutation.path.split('.')
102-
const key = pathArray.pop() as string
103-
const target = pathArray.reduce((aggr, key) => aggr[key], state as any)
104-
const classInstance = pathArray.reduce((aggr, key) => aggr[key], classes as any)
105-
106-
if (mutation.method === 'set') {
107-
if (typeof classInstance === 'function' && Array.isArray(mutation.args[0])) {
108-
target[key] = mutation.args[0].map((arg) => classInstance(arg))
109-
} else if (typeof classInstance === 'function') {
110-
target[key] = classInstance(mutation.args[0])
111-
} else {
112-
target[key] = mutation.args[0]
113-
}
114-
} else if (mutation.method === 'unset') {
115-
delete target[key]
116-
} else {
117-
target[key][mutation.method].apply(target[key], typeof classInstance === 'function' ? mutation.args.map((arg) => {
118-
return typeof arg === 'object' && arg !== null ? classInstance(arg) : arg
119-
}) : mutation.args)
120-
}
121-
})
122-
} else {
123-
rehydrateState(state, source, classes)
124-
}
125-
}
126-
12796
export interface OvermindSSR<Config extends IConfiguration>
12897
extends Overmind<Config> {
12998
hydrate(): IMutation[]

packages/node_modules/overmind/src/internalTypes.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
} from 'proxy-state-tree'
77

88
import { IAction, IOperator, IState } from './types'
9-
import { SERIALIZE } from './utils'
109

1110
export type SubType<Base, Condition> = Pick<
1211
Base,
@@ -17,23 +16,6 @@ export type NestedPartial<T> = T extends Function
1716
? T
1817
: Partial<{ [P in keyof T]: NestedPartial<T[P]> }>
1918

20-
export interface Serialize {
21-
[SERIALIZE]: boolean
22-
}
23-
24-
export type Serializable = Serialize | {
25-
toJSON: () => {
26-
[SERIALIZE]: boolean
27-
}
28-
}
29-
30-
export type RehydrateClasses<T extends IState> = Pick<{
31-
[P in keyof T]: T[P] extends Serializable ? (data: any) => T[P] :
32-
T[P] extends Array<Serializable> ? (data: any) => T[P][keyof T[P]] :
33-
T[P] extends { [key: string]: Serializable } ? (data: any) => T[P][keyof T[P]] :
34-
T[P] extends IState ? RehydrateClasses<T[P]> :
35-
never
36-
},{ [Key in keyof T]: T[Key] extends Serializable | Array<Serializable> | { [key: string]: Serializable } ? Key : never }[keyof T]>
3719

3820
export type Options = {
3921
name?: string
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { rehydrate } from './'
2+
3+
describe('REHYDRATE', () => {
4+
test('should allow rehydration', () => {
5+
expect.assertions(1)
6+
const state = {
7+
foo: 'bar'
8+
}
9+
rehydrate(state, {
10+
foo: 'bar2'
11+
})
12+
13+
expect(state).toEqual({
14+
foo: 'bar2'
15+
})
16+
})
17+
test('should allow rehydration by mutations', () => {
18+
expect.assertions(1)
19+
const state = {
20+
foo: 'bar'
21+
}
22+
rehydrate(state, [{
23+
method: 'set',
24+
args: ['bar2'],
25+
hasChangedValue: false,
26+
path: 'foo',
27+
revert: () => {}
28+
}])
29+
30+
expect(state).toEqual({
31+
foo: 'bar2'
32+
})
33+
})
34+
test('should allow rehydration of single class value', () => {
35+
expect.assertions(1)
36+
class User {
37+
name = 'Bob'
38+
static fromJSON(json) {
39+
return Object.assign(new User(), json)
40+
}
41+
}
42+
type State = {
43+
user: User | null
44+
user2: User
45+
}
46+
const state: State = {
47+
user: null,
48+
user2: new User()
49+
}
50+
rehydrate(state, {
51+
user: {
52+
name: 'Bob2'
53+
},
54+
user2: {
55+
name: 'Bob2'
56+
}
57+
}, {
58+
user: User.fromJSON,
59+
user2: User.fromJSON
60+
})
61+
62+
expect(state).toEqual({
63+
user: {
64+
name: 'Bob2'
65+
},
66+
user2: {
67+
name: 'Bob2'
68+
}
69+
})
70+
})
71+
test('should allow rehydration of array', () => {
72+
expect.assertions(2)
73+
class User {
74+
name = 'Bob'
75+
static fromJSON(json) {
76+
return Object.assign(new User(), json)
77+
}
78+
}
79+
type State = {
80+
users: User[]
81+
}
82+
const state: State = {
83+
users: [],
84+
}
85+
rehydrate(state, {
86+
users: [{
87+
name: 'Bob2'
88+
}, {
89+
name: 'Bob3'
90+
}]
91+
}, {
92+
users: User.fromJSON,
93+
})
94+
95+
expect(state.users[0]).toBeInstanceOf(User)
96+
expect(state.users[1]).toBeInstanceOf(User)
97+
})
98+
test('should allow rehydration of dictionary', () => {
99+
expect.assertions(2)
100+
class User {
101+
name = 'Bob'
102+
static fromJSON(json) {
103+
return Object.assign(new User(), json)
104+
}
105+
}
106+
type State = {
107+
users: { [key: string]: User }
108+
}
109+
const state: State = {
110+
users: {},
111+
}
112+
rehydrate(state, {
113+
users: {
114+
'id1': {
115+
name: 'Bob2'
116+
},
117+
'id2': {
118+
name: 'Bob3'
119+
}
120+
}
121+
}, {
122+
users: User.fromJSON,
123+
})
124+
125+
expect(state.users.id1).toBeInstanceOf(User)
126+
expect(state.users.id2).toBeInstanceOf(User)
127+
})
128+
})
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { IMutation } from 'proxy-state-tree'
2+
3+
import { IState } from './types'
4+
5+
export function rehydrateState(target: any, source: any, classes: any = {}) {
6+
if (!target || !source) {
7+
throw new Error(`You have to pass a "target" and "source" object to rehydrate`)
8+
}
9+
10+
Object.keys(source).forEach((key) => {
11+
const value = source[key]
12+
const classInstance = classes[key]
13+
14+
if (typeof classInstance === 'function' && Array.isArray(target[key])) {
15+
target[key] = (source[key] as any[]).map(value => classInstance(value))
16+
} else if (typeof classInstance === 'function' && typeof target[key] === 'object' && target[key] !== null && target[key].constructor.name === 'Object') {
17+
target[key] = Object.keys(source[key] as any).reduce((aggr, subKey) => {
18+
aggr[subKey] = classInstance((source[key] as any)[subKey])
19+
20+
return aggr
21+
}, {})
22+
} else if (typeof classInstance === 'function') {
23+
target[key] = classInstance(source[key])
24+
} else if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
25+
if (!target[key]) target[key] = {}
26+
rehydrateState(target[key] as IState, source[key] as IState, classes[key])
27+
} else {
28+
target[key] = source[key]
29+
}
30+
})
31+
}
32+
33+
export const SERIALIZE = Symbol('SERIALIZE')
34+
35+
export interface Serialize {
36+
[SERIALIZE]: boolean
37+
}
38+
39+
export type Serializable = Serialize | {
40+
toJSON: () => {
41+
[SERIALIZE]: boolean
42+
}
43+
}
44+
45+
export type RehydrateClasses<T extends IState> = Pick<{
46+
[P in keyof T]: T[P] extends Serializable ? (data: any) => T[P] :
47+
T[P] extends Array<Serializable> ? (data: any) => T[P][keyof T[P]] :
48+
T[P] extends { [key: string]: Serializable } ? (data: any) => T[P][keyof T[P]] :
49+
T[P] extends IState ? RehydrateClasses<T[P]> :
50+
never
51+
},{ [Key in keyof T]: T[Key] extends Serializable | Array<Serializable> | { [key: string]: Serializable } ? Key : never }[keyof T]>
52+
53+
54+
export const rehydrate = <T extends IState>(state: T, source: IMutation[] | IState, classes: RehydrateClasses<T> = {} as any) => {
55+
if (Array.isArray(source)) {
56+
const mutations = source as IMutation[]
57+
mutations.forEach((mutation) => {
58+
const pathArray = mutation.path.split('.')
59+
const key = pathArray.pop() as string
60+
const target = pathArray.reduce((aggr, key) => aggr[key], state as any)
61+
const classInstance = pathArray.reduce((aggr, key) => aggr[key], classes as any)
62+
63+
if (mutation.method === 'set') {
64+
if (typeof classInstance === 'function' && Array.isArray(mutation.args[0])) {
65+
target[key] = mutation.args[0].map((arg) => classInstance(arg))
66+
} else if (typeof classInstance === 'function') {
67+
target[key] = classInstance(mutation.args[0])
68+
} else {
69+
target[key] = mutation.args[0]
70+
}
71+
} else if (mutation.method === 'unset') {
72+
delete target[key]
73+
} else {
74+
target[key][mutation.method].apply(target[key], typeof classInstance === 'function' ? mutation.args.map((arg) => {
75+
return typeof arg === 'object' && arg !== null ? classInstance(arg) : arg
76+
}) : mutation.args)
77+
}
78+
})
79+
} else {
80+
rehydrateState(state, source, classes)
81+
}
82+
}

0 commit comments

Comments
 (0)