jason-rails 0.3.0 → 0.4.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +34 -0
  3. data/README.md +6 -9
  4. data/app/assets/config/jason_engine_manifest.js +1 -0
  5. data/app/assets/images/jason/engine/.keep +0 -0
  6. data/app/assets/stylesheets/jason/engine/application.css +15 -0
  7. data/app/controllers/jason/api_controller.rb +36 -0
  8. data/app/helpers/jason/engine/application_helper.rb +6 -0
  9. data/app/jobs/jason/engine/application_job.rb +6 -0
  10. data/app/mailers/jason/engine/application_mailer.rb +8 -0
  11. data/app/models/jason/engine/application_record.rb +7 -0
  12. data/app/views/layouts/jason/engine/application.html.erb +15 -0
  13. data/client/babel.config.js +13 -0
  14. data/client/lib/JasonProvider.d.ts +5 -4
  15. data/client/lib/JasonProvider.js +30 -3
  16. data/client/lib/actionFactory.js +1 -1
  17. data/client/lib/createActions.d.ts +1 -1
  18. data/client/lib/createActions.js +2 -27
  19. data/client/lib/createJasonReducers.js +1 -0
  20. data/client/lib/createOptDis.d.ts +1 -0
  21. data/client/lib/createOptDis.js +45 -0
  22. data/client/lib/createPayloadHandler.d.ts +1 -1
  23. data/client/lib/createPayloadHandler.js +23 -6
  24. data/client/lib/deepCamelizeKeys.d.ts +1 -0
  25. data/client/lib/deepCamelizeKeys.js +23 -0
  26. data/client/lib/deepCamelizeKeys.test.d.ts +1 -0
  27. data/client/lib/deepCamelizeKeys.test.js +106 -0
  28. data/client/lib/index.d.ts +4 -4
  29. data/client/package.json +17 -4
  30. data/client/src/JasonProvider.tsx +33 -5
  31. data/client/src/actionFactory.ts +1 -1
  32. data/client/src/createActions.ts +2 -33
  33. data/client/src/createJasonReducers.ts +1 -0
  34. data/client/src/createOptDis.ts +47 -0
  35. data/client/src/createPayloadHandler.ts +26 -4
  36. data/client/src/deepCamelizeKeys.test.ts +113 -0
  37. data/client/src/deepCamelizeKeys.ts +17 -0
  38. data/client/yarn.lock +4539 -81
  39. data/config/routes.rb +4 -0
  40. data/jason-rails.gemspec +5 -0
  41. data/lib/jason.rb +7 -1
  42. data/lib/jason/api_model.rb +16 -0
  43. data/lib/jason/channel.rb +2 -2
  44. data/lib/jason/engine.rb +5 -0
  45. data/lib/jason/publisher.rb +38 -6
  46. data/lib/jason/subscription.rb +21 -24
  47. data/lib/jason/version.rb +1 -1
  48. metadata +81 -3
@@ -0,0 +1 @@
1
+ export default function deepCamelizeKeys(item: any, excludeIf?: (k: any) => boolean): any;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const lodash_1 = __importDefault(require("lodash"));
7
+ function deepCamelizeKeys(item, excludeIf = k => false) {
8
+ function camelizeKey(key) {
9
+ if (excludeIf(key))
10
+ return key;
11
+ return lodash_1.default.camelCase(key);
12
+ }
13
+ if (lodash_1.default.isArray(item)) {
14
+ return lodash_1.default.map(item, item => deepCamelizeKeys(item, excludeIf));
15
+ }
16
+ else if (lodash_1.default.isObject(item)) {
17
+ return lodash_1.default.mapValues(lodash_1.default.mapKeys(item, (v, k) => camelizeKey(k)), (v, k) => deepCamelizeKeys(v, excludeIf));
18
+ }
19
+ else {
20
+ return item;
21
+ }
22
+ }
23
+ exports.default = deepCamelizeKeys;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const deepCamelizeKeys_1 = __importDefault(require("./deepCamelizeKeys"));
7
+ test('scalar number', () => {
8
+ expect(deepCamelizeKeys_1.default(1)).toBe(1);
9
+ });
10
+ test('scalar number float', () => {
11
+ expect(deepCamelizeKeys_1.default(1.123)).toBe(1.123);
12
+ });
13
+ test('scalar string', () => {
14
+ expect(deepCamelizeKeys_1.default('test')).toBe('test');
15
+ });
16
+ test('scalar null', () => {
17
+ expect(deepCamelizeKeys_1.default(null)).toBe(null);
18
+ });
19
+ test('scalar boolean', () => {
20
+ expect(deepCamelizeKeys_1.default(true)).toBe(true);
21
+ });
22
+ test('object with existing camelized keys', () => {
23
+ expect(deepCamelizeKeys_1.default({ testMe: 'test' })).toStrictEqual({ testMe: 'test' });
24
+ });
25
+ test('array with existing camelized keys', () => {
26
+ expect(deepCamelizeKeys_1.default([{ testMe: 'test' }, { testMe2: 'test' }])).toStrictEqual([{ testMe: 'test' }, { testMe2: 'test' }]);
27
+ });
28
+ test('object with mixed keys', () => {
29
+ expect(deepCamelizeKeys_1.default({ testMe: 'test', test_2: 'dog', test_me2: true })).toStrictEqual({ testMe: 'test', test2: 'dog', testMe2: true });
30
+ });
31
+ test('array with mixed keys', () => {
32
+ expect(deepCamelizeKeys_1.default([
33
+ { testMe: 'test', test_2: 'dog', test_me2: true },
34
+ { testMe3: 'test', test_3: 'dog', test_me4: true }
35
+ ])).toStrictEqual([
36
+ { testMe: 'test', test2: 'dog', testMe2: true },
37
+ { testMe3: 'test', test3: 'dog', testMe4: true }
38
+ ]);
39
+ });
40
+ test('nested with object at top level', () => {
41
+ expect(deepCamelizeKeys_1.default({
42
+ test_me: {
43
+ test_me2: {
44
+ test_me3: [
45
+ { test_it_out: '49' },
46
+ { test_fun: 'what' }
47
+ ]
48
+ }
49
+ }
50
+ })).toStrictEqual({
51
+ testMe: {
52
+ testMe2: {
53
+ testMe3: [
54
+ { testItOut: '49' },
55
+ { testFun: 'what' }
56
+ ]
57
+ }
58
+ }
59
+ });
60
+ });
61
+ test('nested with object at top level', () => {
62
+ expect(deepCamelizeKeys_1.default([{
63
+ test_me: {
64
+ test_me2: {
65
+ test_me3: [
66
+ { test_it_out: '49' },
67
+ { test_fun: 'what' }
68
+ ]
69
+ }
70
+ }
71
+ }, {
72
+ test_it52: 'what?'
73
+ }])).toStrictEqual([{
74
+ testMe: {
75
+ testMe2: {
76
+ testMe3: [
77
+ { testItOut: '49' },
78
+ { testFun: 'what' }
79
+ ]
80
+ }
81
+ }
82
+ }, {
83
+ testIt52: 'what?'
84
+ }]);
85
+ });
86
+ test('excludes keys by function', () => {
87
+ expect(deepCamelizeKeys_1.default({
88
+ test_me: {
89
+ test_me2: {
90
+ test_me3: [
91
+ { test_it_out: '49' },
92
+ { test_fun: 'what' }
93
+ ]
94
+ }
95
+ }
96
+ }, k => (k === 'test_me2'))).toStrictEqual({
97
+ testMe: {
98
+ test_me2: {
99
+ testMe3: [
100
+ { testItOut: '49' },
101
+ { testFun: 'what' }
102
+ ]
103
+ }
104
+ }
105
+ });
106
+ });
@@ -1,10 +1,10 @@
1
1
  import _useAct from './useAct';
2
2
  import _useSub from './useSub';
3
3
  export declare const JasonProvider: ({ reducers, middleware, extraActions, children }: {
4
- reducers: any;
5
- middleware: any;
6
- extraActions: any;
7
- children: any;
4
+ reducers?: any;
5
+ middleware?: any;
6
+ extraActions?: any;
7
+ children?: any;
8
8
  }) => any;
9
9
  export declare const useAct: typeof _useAct;
10
10
  export declare const useSub: typeof _useSub;
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@jamesr2323/jason",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "module": "./lib/index.js",
5
+ "types": "./lib/index.d.ts",
5
6
  "scripts": {
6
- "build": "tsc"
7
+ "build": "tsc",
8
+ "test": "jest"
7
9
  },
8
10
  "dependencies": {
9
11
  "@rails/actioncable": "^6.0.3-4",
@@ -17,11 +19,22 @@
17
19
  "uuid": "^8.3.1"
18
20
  },
19
21
  "devDependencies": {
22
+ "@babel/core": "^7.12.10",
23
+ "@babel/preset-env": "^7.12.11",
24
+ "@babel/preset-typescript": "^7.12.7",
25
+ "@reduxjs/toolkit": "^1.5.0",
26
+ "@types/jest": "^26.0.19",
27
+ "babel-jest": "^26.6.3",
28
+ "jest": "^26.6.3",
29
+ "react": "^16.8.3",
30
+ "react-dom": "^16.8.3",
31
+ "react-redux": "^7.2.2",
20
32
  "typescript": "^4.1.2"
21
33
  },
22
34
  "peerDependencies": {
35
+ "@reduxjs/toolkit": "^1.5.0",
23
36
  "react": "^16.8.3",
24
- "react-redux": "^7.2.2",
25
- "@reduxjs/toolkit": "^1.5.0"
37
+ "react-dom": "^16.8.3",
38
+ "react-redux": "^7.2.2"
26
39
  }
27
40
  }
@@ -7,26 +7,54 @@ import { Provider } from 'react-redux'
7
7
  import { createEntityAdapter, createSlice, createReducer, configureStore } from '@reduxjs/toolkit'
8
8
  import createJasonReducers from './createJasonReducers'
9
9
  import createPayloadHandler from './createPayloadHandler'
10
+ import createOptDis from './createOptDis'
10
11
  import makeEager from './makeEager'
11
12
  import { camelizeKeys } from 'humps'
12
13
  import md5 from 'blueimp-md5'
13
14
  import _ from 'lodash'
14
15
  import React, { useState, useEffect } from 'react'
16
+ import { validate as isUuid } from 'uuid'
15
17
 
16
- const JasonProvider = ({ reducers, middleware, extraActions, children }) => {
18
+ const JasonProvider = ({ reducers, middleware, extraActions, children }: { reducers?: any, middleware?: any, extraActions?: any, children?: React.FC }) => {
17
19
  const [store, setStore] = useState(null)
18
20
  const [value, setValue] = useState(null)
19
21
  const [connected, setConnected] = useState(false)
20
22
 
21
23
  const csrfToken = (document.querySelector("meta[name=csrf-token]") as any).content
22
24
  axios.defaults.headers.common['X-CSRF-Token'] = csrfToken
23
- const restClient = applyCaseMiddleware(axios.create())
25
+ const restClient = applyCaseMiddleware(axios.create(), {
26
+ preservedKeys: (key) => {
27
+ return isUuid(key)
28
+ }
29
+ })
24
30
 
25
31
  useEffect(() => {
26
32
  restClient.get('/jason/api/schema')
27
33
  .then(({ data: snakey_schema }) => {
28
34
  const schema = camelizeKeys(snakey_schema)
29
35
 
36
+ const serverActionQueue = function() {
37
+ const queue: any[] = []
38
+ let inFlight = false
39
+
40
+ return {
41
+ addItem: (item) => queue.push(item),
42
+ getItem: () => {
43
+ if (inFlight) return false
44
+
45
+ const item = queue.shift()
46
+ if (item) {
47
+ inFlight = true
48
+ return item
49
+ }
50
+ return false
51
+ },
52
+ itemProcessed: () => inFlight = false,
53
+ fullySynced: () => queue.length === 0 && !inFlight,
54
+ getData: () => ({ queue, inFlight })
55
+ }
56
+ }()
57
+
30
58
  const consumer = createConsumer()
31
59
  const allReducers = {
32
60
  ...reducers,
@@ -67,7 +95,7 @@ const JasonProvider = ({ reducers, middleware, extraActions, children }) => {
67
95
  console.log('Subscribe with', config, md5Hash)
68
96
 
69
97
  _.map(config, (v, model) => {
70
- payloadHandlers[`${model}:${md5Hash}`] = createPayloadHandler(store.dispatch, subscription, model, schema[model])
98
+ payloadHandlers[`${model}:${md5Hash}`] = createPayloadHandler(store.dispatch, serverActionQueue, subscription, model, schema[model])
71
99
  })
72
100
  subscription.send({ createSubscription: config })
73
101
 
@@ -81,8 +109,8 @@ const JasonProvider = ({ reducers, middleware, extraActions, children }) => {
81
109
  delete payloadHandlers[`${model}:${md5Hash}`]
82
110
  })
83
111
  }
84
-
85
- const actions = createActions(schema, store, restClient, extraActions)
112
+ const optDis = createOptDis(schema, store.dispatch, restClient, serverActionQueue)
113
+ const actions = createActions(schema, store, restClient, optDis, extraActions)
86
114
  const eager = makeEager(schema)
87
115
 
88
116
  console.log({ actions })
@@ -24,7 +24,7 @@ export default (dis, store, entity, { extraActions = {}, hasPriority = false } =
24
24
  return dis({ type: `${pluralize(entity)}/remove`, payload: id })
25
25
  }
26
26
 
27
- const extraActionsResolved = _.mapValues(extraActions, v => v(dis, store, entity))
27
+ const extraActionsResolved = extraActions ? _.mapValues(extraActions, v => v(dis, store, entity)) : {}
28
28
 
29
29
  if (hasPriority) {
30
30
  return { add, upsert, setAll, remove, movePriority, ...extraActionsResolved }
@@ -1,39 +1,8 @@
1
1
  import actionFactory from './actionFactory'
2
2
  import pluralize from 'pluralize'
3
3
  import _ from 'lodash'
4
- import { v4 as uuidv4 } from 'uuid'
5
-
6
- function enrich(type, payload) {
7
- if (type.split('/')[1] === 'upsert' && !(type.split('/')[0] === 'session')) {
8
- if (!payload.id) {
9
- return { ...payload, id: uuidv4() }
10
- }
11
- }
12
- return payload
13
- }
14
-
15
- function makeOptDis(schema, dispatch, restClient) {
16
- const plurals = _.keys(schema).map(k => pluralize(k))
17
-
18
- return function (action) {
19
- const { type, payload } = action
20
- const data = enrich(type, payload)
21
-
22
- dispatch(action)
23
-
24
- if (plurals.indexOf(type.split('/')[0]) > -1) {
25
- return restClient.post('/jason/api/action', { type, payload: data } )
26
- .catch(e => {
27
- dispatch({ type: 'upsertLocalUi', data: { error: JSON.stringify(e) } })
28
- })
29
- }
30
- }
31
- }
32
-
33
- function createActions(schema, store, restClient, extraActions) {
34
- const dis = store.dispatch;
35
- const optDis = makeOptDis(schema, dis, restClient)
36
4
 
5
+ function createActions(schema, store, restClient, optDis, extraActions) {
37
6
  const actions = _.fromPairs(_.map(schema, (config, model: string) => {
38
7
  if (config.priorityScope) {
39
8
  return [pluralize(model), actionFactory(optDis, store, model, { hasPriority: true })]
@@ -42,7 +11,7 @@ function createActions(schema, store, restClient, extraActions) {
42
11
  }
43
12
  }))
44
13
 
45
- const extraActionsResolved = extraActions(optDis, store, restClient)
14
+ const extraActionsResolved = extraActions ? extraActions(optDis, store, restClient, actions) : {}
46
15
 
47
16
  return _.merge(actions, extraActionsResolved)
48
17
  }
@@ -16,6 +16,7 @@ function generateSlices(schema) {
16
16
  add: adapter.addOne,
17
17
  setAll: adapter.setAll,
18
18
  remove: adapter.removeOne,
19
+ removeMany: adapter.removeMany,
19
20
  movePriority: (s, { payload: { id, priority, parentFilter } }) => {
20
21
  // Get IDs and insert our item at the new index
21
22
  var affectedIds = _.orderBy(_.filter(_.values(s.entities), parentFilter).filter(e => e.id !== id), 'priority').map(e => e.id)
@@ -0,0 +1,47 @@
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
+ let inFlight = false
17
+
18
+ function enqueueServerAction (action) {
19
+ serverActionQueue.addItem(action)
20
+ }
21
+
22
+ function dispatchServerAction() {
23
+ const action = serverActionQueue.getItem()
24
+ if (!action) return
25
+
26
+ inFlight = true
27
+ restClient.post('/jason/api/action', action)
28
+ .then(serverActionQueue.itemProcessed)
29
+ .catch(e => {
30
+ dispatch({ type: 'upsertLocalUi', data: { error: JSON.stringify(e) } })
31
+ serverActionQueue.itemProcessed()
32
+ })
33
+ }
34
+
35
+ setInterval(dispatchServerAction, 10)
36
+
37
+ return function (action) {
38
+ const { type, payload } = action
39
+ const data = enrich(type, payload)
40
+
41
+ dispatch({ type, payload: data })
42
+
43
+ if (plurals.indexOf(type.split('/')[0]) > -1) {
44
+ enqueueServerAction({ type, payload: data })
45
+ }
46
+ }
47
+ }
@@ -1,16 +1,18 @@
1
1
  import { apply_patch } from 'jsonpatch'
2
- import { camelizeKeys } from 'humps'
2
+ import deepCamelizeKeys from './deepCamelizeKeys'
3
3
  import pluralize from 'pluralize'
4
4
  import _ from 'lodash'
5
+ import { validate as isUuid } 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, subscription, model, config) {
12
+ export default function createPayloadHandler(dispatch, serverActionQueue, subscription, model, config) {
12
13
  console.log({ model, config })
13
- let payload = {}
14
+ let payload = [] as any[]
15
+ let previousPayload = [] as any[]
14
16
  let idx = 0
15
17
  let patchQueue = {}
16
18
 
@@ -23,20 +25,40 @@ export default function createPayloadHandler(dispatch, subscription, model, conf
23
25
  subscription.send({ getPayload: { model, config } })
24
26
  }
25
27
 
28
+ function camelizeKeys(item) {
29
+ return deepCamelizeKeys(item, key => isUuid(key))
30
+ }
31
+
26
32
  const tGetPayload = _.throttle(getPayload, 10000)
27
33
 
28
34
  function dispatchPayload() {
35
+ // We want to avoid updates from server overwriting changes to local state, so if there is a queue then wait.
36
+ if (!serverActionQueue.fullySynced()) {
37
+ console.log(serverActionQueue.getData())
38
+ setTimeout(dispatchPayload, 100)
39
+ return
40
+ }
41
+
29
42
  const includeModels = (config.includeModels || []).map(m => _.camelCase(m))
30
43
 
31
44
  console.log("Dispatching", { payload, includeModels })
32
45
 
33
46
  includeModels.forEach(m => {
34
47
  const subPayload = _.flatten(_.compact(camelizeKeys(payload).map(instance => instance[m])))
35
- console.log({ type: `${pluralize(m)}/upsertMany`, payload: subPayload })
48
+ const previousSubPayload = _.flatten(_.compact(camelizeKeys(previousPayload).map(instance => instance[m])))
49
+
50
+ // Find IDs that were in the payload but are no longer
51
+ const idsToRemove = _.difference(previousSubPayload.map(i => i.id), subPayload.map(i => i.id))
52
+
36
53
  dispatch({ type: `${pluralize(m)}/upsertMany`, payload: subPayload })
54
+ dispatch({ type: `${pluralize(m)}/removeMany`, payload: idsToRemove })
37
55
  })
38
56
 
57
+ const idsToRemove = _.difference(previousPayload.map(i => i.id), payload.map(i => i.id))
58
+
39
59
  dispatch({ type: `${pluralize(model)}/upsertMany`, payload: camelizeKeys(payload) })
60
+ dispatch({ type: `${pluralize(model)}/removeMany`, payload: idsToRemove })
61
+ previousPayload = payload
40
62
  }
41
63
 
42
64
  function processQueue() {