jason-rails 0.3.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/.ruby-version +1 -0
- data/Gemfile.lock +184 -0
- data/README.md +118 -10
- data/app/controllers/jason/api/pusher_controller.rb +15 -0
- data/app/controllers/jason/api_controller.rb +78 -0
- data/client/babel.config.js +13 -0
- data/client/lib/JasonContext.d.ts +6 -1
- data/client/lib/JasonProvider.d.ts +6 -5
- data/client/lib/JasonProvider.js +5 -97
- data/client/lib/actionFactory.js +1 -1
- data/client/lib/createActions.d.ts +1 -1
- data/client/lib/createActions.js +2 -27
- data/client/lib/createJasonReducers.js +49 -3
- data/client/lib/createOptDis.d.ts +1 -0
- data/client/lib/createOptDis.js +43 -0
- data/client/lib/createPayloadHandler.d.ts +9 -1
- data/client/lib/createPayloadHandler.js +52 -43
- data/client/lib/createServerActionQueue.d.ts +10 -0
- data/client/lib/createServerActionQueue.js +48 -0
- data/client/lib/createServerActionQueue.test.d.ts +1 -0
- data/client/lib/createServerActionQueue.test.js +37 -0
- data/client/lib/createTransportAdapter.d.ts +5 -0
- data/client/lib/createTransportAdapter.js +20 -0
- data/client/lib/deepCamelizeKeys.d.ts +1 -0
- data/client/lib/deepCamelizeKeys.js +23 -0
- data/client/lib/deepCamelizeKeys.test.d.ts +1 -0
- data/client/lib/deepCamelizeKeys.test.js +106 -0
- data/client/lib/index.d.ts +6 -5
- data/client/lib/pruneIdsMiddleware.d.ts +2 -0
- data/client/lib/pruneIdsMiddleware.js +24 -0
- data/client/lib/restClient.d.ts +2 -0
- data/client/lib/restClient.js +17 -0
- data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
- data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
- data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
- data/client/lib/transportAdapters/pusherAdapter.js +68 -0
- data/client/lib/useJason.d.ts +5 -0
- data/client/lib/useJason.js +94 -0
- data/client/lib/useJason.test.d.ts +1 -0
- data/client/lib/useJason.test.js +85 -0
- data/client/lib/useSub.d.ts +1 -1
- data/client/lib/useSub.js +6 -3
- data/client/package.json +19 -4
- data/client/src/JasonProvider.tsx +6 -96
- data/client/src/actionFactory.ts +1 -1
- data/client/src/createActions.ts +2 -33
- data/client/src/createJasonReducers.ts +57 -3
- data/client/src/createOptDis.ts +45 -0
- data/client/src/createPayloadHandler.ts +58 -47
- data/client/src/createServerActionQueue.test.ts +42 -0
- data/client/src/createServerActionQueue.ts +47 -0
- data/client/src/createTransportAdapter.ts +13 -0
- data/client/src/deepCamelizeKeys.test.ts +113 -0
- data/client/src/deepCamelizeKeys.ts +17 -0
- data/client/src/pruneIdsMiddleware.ts +24 -0
- data/client/src/restClient.ts +14 -0
- data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
- data/client/src/transportAdapters/pusherAdapter.ts +72 -0
- data/client/src/useJason.test.ts +87 -0
- data/client/src/useJason.ts +110 -0
- data/client/src/useSub.ts +6 -3
- data/client/yarn.lock +4607 -81
- data/config/routes.rb +8 -0
- data/jason-rails.gemspec +9 -0
- data/lib/jason.rb +40 -1
- data/lib/jason/api_model.rb +15 -9
- data/lib/jason/broadcaster.rb +19 -0
- data/lib/jason/channel.rb +50 -21
- data/lib/jason/engine.rb +5 -0
- data/lib/jason/graph_helper.rb +165 -0
- data/lib/jason/includes_helper.rb +108 -0
- data/lib/jason/lua_generator.rb +71 -0
- data/lib/jason/publisher.rb +103 -30
- data/lib/jason/publisher_old.rb +112 -0
- data/lib/jason/subscription.rb +352 -101
- data/lib/jason/subscription_old.rb +171 -0
- data/lib/jason/version.rb +1 -1
- metadata +151 -4
@@ -0,0 +1,45 @@
|
|
1
|
+
import _ from 'lodash'
|
2
|
+
import pluralize from 'pluralize'
|
3
|
+
import { v4 as uuidv4 } from 'uuid'
|
4
|
+
|
5
|
+
function enrich(type, payload) {
|
6
|
+
if (type.split('/')[1] === 'upsert' && !(type.split('/')[0] === 'session')) {
|
7
|
+
if (!payload.id) {
|
8
|
+
return { ...payload, id: uuidv4() }
|
9
|
+
}
|
10
|
+
}
|
11
|
+
return payload
|
12
|
+
}
|
13
|
+
|
14
|
+
export default function createOptDis(schema, dispatch, restClient, serverActionQueue) {
|
15
|
+
const plurals = _.keys(schema).map(k => pluralize(k))
|
16
|
+
|
17
|
+
function enqueueServerAction (action) {
|
18
|
+
serverActionQueue.addItem(action)
|
19
|
+
}
|
20
|
+
|
21
|
+
function dispatchServerAction() {
|
22
|
+
const action = serverActionQueue.getItem()
|
23
|
+
if (!action) return
|
24
|
+
|
25
|
+
restClient.post('/jason/api/action', action)
|
26
|
+
.then(serverActionQueue.itemProcessed)
|
27
|
+
.catch(e => {
|
28
|
+
dispatch({ type: 'upsertLocalUi', data: { error: JSON.stringify(e) } })
|
29
|
+
serverActionQueue.itemProcessed()
|
30
|
+
})
|
31
|
+
}
|
32
|
+
|
33
|
+
setInterval(dispatchServerAction, 10)
|
34
|
+
|
35
|
+
return function (action) {
|
36
|
+
const { type, payload } = action
|
37
|
+
const data = enrich(type, payload)
|
38
|
+
|
39
|
+
dispatch({ type, payload: data })
|
40
|
+
|
41
|
+
if (plurals.indexOf(type.split('/')[0]) > -1) {
|
42
|
+
enqueueServerAction({ type, payload: data })
|
43
|
+
}
|
44
|
+
}
|
45
|
+
}
|
@@ -1,17 +1,18 @@
|
|
1
1
|
import { apply_patch } from 'jsonpatch'
|
2
|
-
import
|
2
|
+
import deepCamelizeKeys from './deepCamelizeKeys'
|
3
3
|
import pluralize from 'pluralize'
|
4
4
|
import _ from 'lodash'
|
5
|
+
import { validate as isUuid, v4 as uuidv4 } from 'uuid'
|
5
6
|
|
6
7
|
function diffSeconds(dt2, dt1) {
|
7
8
|
var diff =(dt2.getTime() - dt1.getTime()) / 1000
|
8
9
|
return Math.abs(Math.round(diff))
|
9
10
|
}
|
10
11
|
|
11
|
-
export default function createPayloadHandler(dispatch,
|
12
|
-
|
13
|
-
|
14
|
-
let idx =
|
12
|
+
export default function createPayloadHandler({ dispatch, serverActionQueue, transportAdapter, config }) {
|
13
|
+
const subscriptionId = uuidv4()
|
14
|
+
|
15
|
+
let idx = {}
|
15
16
|
let patchQueue = {}
|
16
17
|
|
17
18
|
let lastCheckAt = new Date()
|
@@ -19,77 +20,87 @@ export default function createPayloadHandler(dispatch, subscription, model, conf
|
|
19
20
|
let checkInterval
|
20
21
|
|
21
22
|
function getPayload() {
|
22
|
-
|
23
|
-
subscription.send({ getPayload: { model, config } })
|
23
|
+
setTimeout(() => transportAdapter.getPayload(config), 1000)
|
24
24
|
}
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
function dispatchPayload() {
|
29
|
-
const includeModels = (config.includeModels || []).map(m => _.camelCase(m))
|
30
|
-
|
31
|
-
console.log("Dispatching", { payload, includeModels })
|
32
|
-
|
33
|
-
includeModels.forEach(m => {
|
34
|
-
const subPayload = _.flatten(_.compact(camelizeKeys(payload).map(instance => instance[m])))
|
35
|
-
console.log({ type: `${pluralize(m)}/upsertMany`, payload: subPayload })
|
36
|
-
dispatch({ type: `${pluralize(m)}/upsertMany`, payload: subPayload })
|
37
|
-
})
|
38
|
-
|
39
|
-
dispatch({ type: `${pluralize(model)}/upsertMany`, payload: camelizeKeys(payload) })
|
26
|
+
function camelizeKeys(item) {
|
27
|
+
return deepCamelizeKeys(item, key => isUuid(key))
|
40
28
|
}
|
41
29
|
|
42
|
-
|
43
|
-
|
30
|
+
const tGetPayload = _.throttle(getPayload, 10000)
|
31
|
+
|
32
|
+
function processQueue(model) {
|
33
|
+
console.debug("processQueue", model, idx[model], patchQueue[model])
|
44
34
|
lastCheckAt = new Date()
|
45
|
-
if (patchQueue[idx]) {
|
46
|
-
|
47
|
-
|
48
|
-
|
35
|
+
if (patchQueue[model][idx[model]]) {
|
36
|
+
if (!serverActionQueue.fullySynced()) {
|
37
|
+
console.debug(serverActionQueue.getData())
|
38
|
+
setTimeout(() => processQueue(model), 100)
|
39
|
+
return
|
49
40
|
}
|
50
|
-
|
51
|
-
idx
|
41
|
+
|
42
|
+
const { payload, destroy, id, type } = patchQueue[model][idx[model]]
|
43
|
+
|
44
|
+
if (type === 'payload') {
|
45
|
+
dispatch({ type: `${pluralize(model)}/upsertMany`, payload })
|
46
|
+
const ids = payload.map(instance => instance.id)
|
47
|
+
dispatch({ type: `jasonModels/setSubscriptionIds`, payload: { model, subscriptionId, ids }})
|
48
|
+
} else if (destroy) {
|
49
|
+
// Middleware will determine if this model should be removed if it isn't in any other subscriptions
|
50
|
+
dispatch({ type: `jasonModels/removeSubscriptionId`, payload: { model, subscriptionId, id }})
|
51
|
+
} else {
|
52
|
+
dispatch({ type: `${pluralize(model)}/upsert`, payload })
|
53
|
+
dispatch({ type: `jasonModels/addSubscriptionId`, payload: { model, subscriptionId, id }})
|
54
|
+
}
|
55
|
+
|
56
|
+
delete patchQueue[model][idx[model]]
|
57
|
+
idx[model]++
|
52
58
|
updateDeadline = null
|
53
|
-
processQueue()
|
59
|
+
processQueue(model)
|
54
60
|
// If there are updates in the queue that are ahead of the index, some have arrived out of order
|
55
61
|
// Set a deadline for new updates before it declares the update missing and refetches.
|
56
|
-
} else if (_.keys(patchQueue).length > 0 && !updateDeadline) {
|
62
|
+
} else if (_.keys(patchQueue[model]).length > 0 && !updateDeadline) {
|
57
63
|
var t = new Date()
|
58
64
|
t.setSeconds(t.getSeconds() + 3)
|
59
65
|
updateDeadline = t
|
60
|
-
setTimeout(processQueue, 3100)
|
66
|
+
setTimeout(() => processQueue(model), 3100)
|
61
67
|
// If more than 10 updates in queue, or deadline has passed, restart
|
62
|
-
} else if (_.keys(patchQueue).length > 10 || (updateDeadline && diffSeconds(updateDeadline, new Date()) < 0)) {
|
68
|
+
} else if (_.keys(patchQueue[model]).length > 10 || (updateDeadline && diffSeconds(updateDeadline, new Date()) < 0)) {
|
63
69
|
tGetPayload()
|
64
70
|
updateDeadline = null
|
65
71
|
}
|
66
72
|
}
|
67
73
|
|
68
74
|
function handlePayload(data) {
|
69
|
-
const {
|
70
|
-
|
75
|
+
const { idx: newIdx, model: snake_model, type } = data
|
76
|
+
const model = _.camelCase(snake_model)
|
71
77
|
|
72
|
-
|
73
|
-
|
78
|
+
idx[model] = idx[model] || 0
|
79
|
+
patchQueue[model] = patchQueue[model] || {}
|
74
80
|
|
75
|
-
|
76
|
-
|
77
|
-
idx = newIdx + 1
|
81
|
+
if (type === 'payload') {
|
82
|
+
idx[model] = newIdx
|
78
83
|
// Clear any old changes left in the queue
|
79
|
-
patchQueue= _.pick(patchQueue, _.keys(patchQueue).filter(k => k > newIdx + 1))
|
80
|
-
return
|
84
|
+
patchQueue[model] = _.pick(patchQueue[model], _.keys(patchQueue[model]).filter(k => k > newIdx + 1))
|
81
85
|
}
|
82
86
|
|
83
|
-
patchQueue[newIdx] =
|
84
|
-
|
85
|
-
processQueue()
|
87
|
+
patchQueue[model][newIdx] = camelizeKeys({ ...data, model })
|
88
|
+
console.debug("Added to queue", model, idx[model], camelizeKeys({ ...data, model }), serverActionQueue.getData())
|
89
|
+
processQueue(model)
|
86
90
|
|
87
91
|
if (diffSeconds((new Date()), lastCheckAt) >= 3) {
|
88
92
|
lastCheckAt = new Date()
|
89
|
-
console.
|
93
|
+
console.debug('Interval lost. Pulling from server')
|
90
94
|
tGetPayload()
|
91
95
|
}
|
92
96
|
}
|
93
97
|
|
94
|
-
|
98
|
+
tGetPayload()
|
99
|
+
|
100
|
+
// Clean up after ourselves
|
101
|
+
function tearDown() {
|
102
|
+
dispatch({ type: `jasonModels/removeSubscription`, payload: { subscriptionId }})
|
103
|
+
}
|
104
|
+
|
105
|
+
return { handlePayload, tearDown }
|
95
106
|
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import createServerActionQueue from './createServerActionQueue'
|
2
|
+
|
3
|
+
test('Adding items', () => {
|
4
|
+
const serverActionQueue = createServerActionQueue()
|
5
|
+
serverActionQueue.addItem({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } })
|
6
|
+
const item = serverActionQueue.getItem()
|
7
|
+
expect(item).toStrictEqual({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } })
|
8
|
+
})
|
9
|
+
|
10
|
+
test('Deduping of items that will overwrite each other', () => {
|
11
|
+
const serverActionQueue = createServerActionQueue()
|
12
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
13
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2 } })
|
14
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 3 } })
|
15
|
+
|
16
|
+
const item = serverActionQueue.getItem()
|
17
|
+
|
18
|
+
expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 3 } })
|
19
|
+
})
|
20
|
+
|
21
|
+
test('Deduping of items with a superset', () => {
|
22
|
+
const serverActionQueue = createServerActionQueue()
|
23
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
24
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } })
|
25
|
+
|
26
|
+
const item = serverActionQueue.getItem()
|
27
|
+
|
28
|
+
expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } })
|
29
|
+
})
|
30
|
+
|
31
|
+
test("doesn't dedupe items with some attributes missing", () => {
|
32
|
+
const serverActionQueue = createServerActionQueue()
|
33
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
34
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute2: 'test' } })
|
35
|
+
|
36
|
+
const item = serverActionQueue.getItem()
|
37
|
+
serverActionQueue.itemProcessed()
|
38
|
+
const item2 = serverActionQueue.getItem()
|
39
|
+
|
40
|
+
expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
41
|
+
expect(item2).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute2: 'test' } })
|
42
|
+
})
|
@@ -0,0 +1,47 @@
|
|
1
|
+
// A FIFO queue with deduping of actions whose effect will be cancelled by later actions
|
2
|
+
|
3
|
+
import _ from 'lodash'
|
4
|
+
|
5
|
+
export default function createServerActionQueue() {
|
6
|
+
const queue: any[] = []
|
7
|
+
let inFlight = false
|
8
|
+
|
9
|
+
function addItem(item) {
|
10
|
+
// Check if there are any items ahead in the queue that this item would effectively overwrite.
|
11
|
+
// In that case we can remove them
|
12
|
+
// If this is an upsert && item ID is the same && current item attributes are a superset of the earlier item attributes
|
13
|
+
const { type, payload } = item
|
14
|
+
if (type.split('/')[1] !== 'upsert') {
|
15
|
+
queue.push(item)
|
16
|
+
return
|
17
|
+
}
|
18
|
+
|
19
|
+
_.remove(queue, item => {
|
20
|
+
const { type: itemType, payload: itemPayload } = item
|
21
|
+
if (type !== itemType) return false
|
22
|
+
if (itemPayload.id !== payload.id) return false
|
23
|
+
|
24
|
+
// Check that all keys of itemPayload are in payload.
|
25
|
+
return _.difference(_.keys(itemPayload),_.keys(payload)).length === 0
|
26
|
+
})
|
27
|
+
|
28
|
+
queue.push(item)
|
29
|
+
}
|
30
|
+
|
31
|
+
return {
|
32
|
+
addItem,
|
33
|
+
getItem: () => {
|
34
|
+
if (inFlight) return false
|
35
|
+
|
36
|
+
const item = queue.shift()
|
37
|
+
if (item) {
|
38
|
+
inFlight = true
|
39
|
+
return item
|
40
|
+
}
|
41
|
+
return false
|
42
|
+
},
|
43
|
+
itemProcessed: () => inFlight = false,
|
44
|
+
fullySynced: () => queue.length === 0 && !inFlight,
|
45
|
+
getData: () => ({ queue, inFlight })
|
46
|
+
}
|
47
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import actionCableAdapter from './transportAdapters/actionCableAdapter'
|
2
|
+
import pusherAdapter from './transportAdapters/pusherAdapter'
|
3
|
+
|
4
|
+
export default function createTransportAdapter(jasonConfig, handlePayload, dispatch, onConnect) {
|
5
|
+
const { transportService } = jasonConfig
|
6
|
+
if (transportService === 'action_cable') {
|
7
|
+
return actionCableAdapter(jasonConfig, handlePayload, dispatch, onConnect)
|
8
|
+
} else if (transportService === 'pusher') {
|
9
|
+
return pusherAdapter(jasonConfig, handlePayload, dispatch)
|
10
|
+
} else {
|
11
|
+
throw(`Transport adapter does not exist for ${transportService}`)
|
12
|
+
}
|
13
|
+
}
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import deepCamelizeKeys from './deepCamelizeKeys'
|
2
|
+
|
3
|
+
test('scalar number', () => {
|
4
|
+
expect(deepCamelizeKeys(1)).toBe(1)
|
5
|
+
})
|
6
|
+
|
7
|
+
test('scalar number float', () => {
|
8
|
+
expect(deepCamelizeKeys(1.123)).toBe(1.123)
|
9
|
+
})
|
10
|
+
|
11
|
+
test('scalar string', () => {
|
12
|
+
expect(deepCamelizeKeys('test')).toBe('test')
|
13
|
+
})
|
14
|
+
|
15
|
+
test('scalar null', () => {
|
16
|
+
expect(deepCamelizeKeys(null)).toBe(null)
|
17
|
+
})
|
18
|
+
|
19
|
+
test('scalar boolean', () => {
|
20
|
+
expect(deepCamelizeKeys(true)).toBe(true)
|
21
|
+
})
|
22
|
+
|
23
|
+
test('object with existing camelized keys', () => {
|
24
|
+
expect(deepCamelizeKeys({ testMe: 'test' })).toStrictEqual({ testMe: 'test' })
|
25
|
+
})
|
26
|
+
|
27
|
+
test('array with existing camelized keys', () => {
|
28
|
+
expect(deepCamelizeKeys([{ testMe: 'test' }, { testMe2: 'test' }])).toStrictEqual([{ testMe: 'test' }, { testMe2: 'test' }])
|
29
|
+
})
|
30
|
+
|
31
|
+
test('object with mixed keys', () => {
|
32
|
+
expect(deepCamelizeKeys({ testMe: 'test', test_2: 'dog', test_me2: true })).toStrictEqual({ testMe: 'test', test2: 'dog', testMe2: true })
|
33
|
+
})
|
34
|
+
|
35
|
+
test('array with mixed keys', () => {
|
36
|
+
expect(deepCamelizeKeys([
|
37
|
+
{ testMe: 'test', test_2: 'dog', test_me2: true },
|
38
|
+
{ testMe3: 'test', test_3: 'dog', test_me4: true }
|
39
|
+
])).toStrictEqual([
|
40
|
+
{ testMe: 'test', test2: 'dog', testMe2: true },
|
41
|
+
{ testMe3: 'test', test3: 'dog', testMe4: true }
|
42
|
+
])
|
43
|
+
})
|
44
|
+
|
45
|
+
test('nested with object at top level', () => {
|
46
|
+
expect(deepCamelizeKeys({
|
47
|
+
test_me: {
|
48
|
+
test_me2: {
|
49
|
+
test_me3: [
|
50
|
+
{ test_it_out: '49' },
|
51
|
+
{ test_fun: 'what' }
|
52
|
+
]
|
53
|
+
}
|
54
|
+
}
|
55
|
+
})).toStrictEqual({
|
56
|
+
testMe: {
|
57
|
+
testMe2: {
|
58
|
+
testMe3: [
|
59
|
+
{ testItOut: '49' },
|
60
|
+
{ testFun: 'what' }
|
61
|
+
]
|
62
|
+
}
|
63
|
+
}
|
64
|
+
})
|
65
|
+
})
|
66
|
+
|
67
|
+
test('nested with object at top level', () => {
|
68
|
+
expect(deepCamelizeKeys([{
|
69
|
+
test_me: {
|
70
|
+
test_me2: {
|
71
|
+
test_me3: [
|
72
|
+
{ test_it_out: '49' },
|
73
|
+
{ test_fun: 'what' }
|
74
|
+
]
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}, {
|
78
|
+
test_it52: 'what?'
|
79
|
+
}])).toStrictEqual([{
|
80
|
+
testMe: {
|
81
|
+
testMe2: {
|
82
|
+
testMe3: [
|
83
|
+
{ testItOut: '49' },
|
84
|
+
{ testFun: 'what' }
|
85
|
+
]
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}, {
|
89
|
+
testIt52: 'what?'
|
90
|
+
}])
|
91
|
+
})
|
92
|
+
|
93
|
+
test('excludes keys by function', () => {
|
94
|
+
expect(deepCamelizeKeys({
|
95
|
+
test_me: {
|
96
|
+
test_me2: {
|
97
|
+
test_me3: [
|
98
|
+
{ test_it_out: '49' },
|
99
|
+
{ test_fun: 'what' }
|
100
|
+
]
|
101
|
+
}
|
102
|
+
}
|
103
|
+
}, k => (k === 'test_me2'))).toStrictEqual({
|
104
|
+
testMe: {
|
105
|
+
test_me2: {
|
106
|
+
testMe3: [
|
107
|
+
{ testItOut: '49' },
|
108
|
+
{ testFun: 'what' }
|
109
|
+
]
|
110
|
+
}
|
111
|
+
}
|
112
|
+
})
|
113
|
+
})
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import _ from 'lodash'
|
2
|
+
|
3
|
+
export default function deepCamelizeKeys(item, excludeIf = k => false) {
|
4
|
+
function camelizeKey(key) {
|
5
|
+
if (excludeIf(key)) return key
|
6
|
+
return _.camelCase(key)
|
7
|
+
}
|
8
|
+
|
9
|
+
if (_.isArray(item)) {
|
10
|
+
return _.map(item, item => deepCamelizeKeys(item, excludeIf))
|
11
|
+
} else if (_.isObject(item)) {
|
12
|
+
return _.mapValues(_.mapKeys(item, (v, k) => camelizeKey(k)), (v, k) => deepCamelizeKeys(v, excludeIf))
|
13
|
+
} else {
|
14
|
+
return item
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import _ from 'lodash'
|
2
|
+
import pluralize from 'pluralize'
|
3
|
+
|
4
|
+
const pruneIdsMiddleware = schema => store => next => action => {
|
5
|
+
const { type, payload } = action
|
6
|
+
const result = next(action)
|
7
|
+
|
8
|
+
const state = store.getState()
|
9
|
+
if (type === 'jasonModels/setSubscriptionIds' || type === 'jasonModels/removeSubscriptionIds') {
|
10
|
+
const { model, ids } = payload
|
11
|
+
|
12
|
+
let idsInSubs = []
|
13
|
+
_.map(state.jasonModels[model], (subscribedIds, k) => {
|
14
|
+
idsInSubs = _.union(idsInSubs, subscribedIds)
|
15
|
+
})
|
16
|
+
// Find IDs currently in Redux that aren't in any subscription
|
17
|
+
const idsToRemove = _.difference(state[pluralize(model)].ids, idsInSubs)
|
18
|
+
store.dispatch({ type: `${pluralize(model)}/removeMany`, payload: idsToRemove })
|
19
|
+
}
|
20
|
+
|
21
|
+
return result
|
22
|
+
}
|
23
|
+
|
24
|
+
export default pruneIdsMiddleware
|