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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2883964fb830dbb506a0e845caa3398436d72262f72cc6b295efce14dcc96346
4
- data.tar.gz: 631d00d04195b1e1f48111ca5a6cc02775488e48f94ab5d8d29783a66a6c91a8
3
+ metadata.gz: 4231946f3742dee57b9eb0eff4a7367691a1ddff2a2842fbba8ffde0c4f5d460
4
+ data.tar.gz: c701f79deb2e23b464b4f15f96442084fb4837d429e87715007b6cc41e31ba9c
5
5
  SHA512:
6
- metadata.gz: 6de81fcdba79fc067fe7515ad83be895e30d19686e548bfb774026993ab965cf6f0aaa9cebcd4807448e5218c8c9609f414e6ec05d5eb717aef880e3f2e012ea
7
- data.tar.gz: adb6476af69fe904588f55214b930959a0b0d1d2357c3edafca77cc4f8d528a72cb0f72cde6dbec4e097e58d6e5758a2c429419cd9ad60f02d0352dd9abe385b
6
+ metadata.gz: 1222df088d647e53b24a735cd9c85b1cf926831171cb21ebd628b67f12fb320003fca93272d94e8173b13d30c21c6ca3cbd55899d604bb2d572196efeb56e8b3
7
+ data.tar.gz: 34ba69d74e1a6729bd88695ff56d62ea40abf3a804065c9803380bd16c948b9cfea51d33eae986cdc3e240db8f051cda2678a63bebfa87e2c144484f2f0f027b
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ jason-rails (0.3.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.4.4)
10
+ rake (12.3.3)
11
+ rspec (3.10.0)
12
+ rspec-core (~> 3.10.0)
13
+ rspec-expectations (~> 3.10.0)
14
+ rspec-mocks (~> 3.10.0)
15
+ rspec-core (3.10.0)
16
+ rspec-support (~> 3.10.0)
17
+ rspec-expectations (3.10.0)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.10.0)
20
+ rspec-mocks (3.10.0)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.10.0)
23
+ rspec-support (3.10.0)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ jason-rails!
30
+ rake (~> 12.0)
31
+ rspec (~> 3.0)
32
+
33
+ BUNDLED WITH
34
+ 1.17.3
data/README.md CHANGED
@@ -14,23 +14,20 @@ Jason attempts to minimize this repitition by auto-generating API endpoints, red
14
14
 
15
15
  ## Installation
16
16
 
17
- Add this line to your application's Gemfile:
17
+ Add the gem and the NPM package
18
18
 
19
19
  ```ruby
20
20
  gem 'jason-rails'
21
21
  ```
22
22
 
23
- And then execute:
24
-
25
- $ bundle install
26
-
27
- Or install it yourself as:
28
-
29
- $ gem install jason
23
+ ```bash
24
+ yarn add @jamesr2323/jason
25
+ ```
30
26
 
31
27
  ## Usage
32
28
 
33
- TODO: Write usage instructions here
29
+ ### Define your schema
30
+
34
31
 
35
32
  ## Development
36
33
 
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/jason/engine .css
File without changes
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,36 @@
1
+ class Jason::ApiController < ::ApplicationController
2
+ def schema
3
+ render json: JASON_API_MODEL.to_json
4
+ end
5
+
6
+ def action
7
+ type = params[:type]
8
+ entity = type.split('/')[0].underscore
9
+ api_model = Jason::ApiModel.new(entity.singularize)
10
+ model = entity.singularize.camelize.constantize
11
+ action = type.split('/')[1].underscore
12
+
13
+ if action == 'move_priority'
14
+ id, priority = params[:payload].values_at(:id, :priority)
15
+
16
+ instance = model.find(id)
17
+ priority_filter = instance.as_json.with_indifferent_access.slice(*api_model.priority_scope)
18
+
19
+ all_instance_ids = model.send(api_model.scope || :all).where(priority_filter).where.not(id: instance.id).order(:priority).pluck(:id)
20
+ all_instance_ids.insert(priority.to_i, instance.id)
21
+
22
+ all_instance_ids.each_with_index do |id, i|
23
+ model.find(id).update!(priority: i, skip_publish_json: true)
24
+ end
25
+
26
+ model.publish_all(model.find(all_instance_ids))
27
+ elsif action == 'upsert' || action == 'add'
28
+ payload = api_model.permit(params)
29
+ return render json: model.find_or_create_by_id!(payload).as_json(api_model.as_json_config)
30
+ elsif action == 'remove'
31
+ model.find(params[:payload]).destroy!
32
+ end
33
+
34
+ return head :ok
35
+ end
36
+ end
@@ -0,0 +1,6 @@
1
+ module Jason
2
+ module Engine
3
+ module ApplicationHelper
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Jason
2
+ module Engine
3
+ class ApplicationJob < ActiveJob::Base
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module Jason
2
+ module Engine
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: 'from@example.com'
5
+ layout 'mailer'
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module Jason
2
+ module Engine
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Jason engine</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "jason/engine/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ presets: [
3
+ [
4
+ '@babel/preset-env',
5
+ {
6
+ targets: {
7
+ node: 'current',
8
+ },
9
+ },
10
+ ],
11
+ '@babel/preset-typescript'
12
+ ],
13
+ };
@@ -1,7 +1,8 @@
1
+ import React from 'react';
1
2
  declare const JasonProvider: ({ reducers, middleware, extraActions, children }: {
2
- reducers: any;
3
- middleware: any;
4
- extraActions: any;
5
- children: any;
3
+ reducers?: any;
4
+ middleware?: any;
5
+ extraActions?: any;
6
+ children?: any;
6
7
  }) => any;
7
8
  export default JasonProvider;
@@ -31,22 +31,48 @@ const react_redux_1 = require("react-redux");
31
31
  const toolkit_1 = require("@reduxjs/toolkit");
32
32
  const createJasonReducers_1 = __importDefault(require("./createJasonReducers"));
33
33
  const createPayloadHandler_1 = __importDefault(require("./createPayloadHandler"));
34
+ const createOptDis_1 = __importDefault(require("./createOptDis"));
34
35
  const makeEager_1 = __importDefault(require("./makeEager"));
35
36
  const humps_1 = require("humps");
36
37
  const blueimp_md5_1 = __importDefault(require("blueimp-md5"));
37
38
  const lodash_1 = __importDefault(require("lodash"));
38
39
  const react_1 = __importStar(require("react"));
40
+ const uuid_1 = require("uuid");
39
41
  const JasonProvider = ({ reducers, middleware, extraActions, children }) => {
40
42
  const [store, setStore] = react_1.useState(null);
41
43
  const [value, setValue] = react_1.useState(null);
42
44
  const [connected, setConnected] = react_1.useState(false);
43
45
  const csrfToken = document.querySelector("meta[name=csrf-token]").content;
44
46
  axios_1.default.defaults.headers.common['X-CSRF-Token'] = csrfToken;
45
- const restClient = axios_case_converter_1.default(axios_1.default.create());
47
+ const restClient = axios_case_converter_1.default(axios_1.default.create(), {
48
+ preservedKeys: (key) => {
49
+ return uuid_1.validate(key);
50
+ }
51
+ });
46
52
  react_1.useEffect(() => {
47
53
  restClient.get('/jason/api/schema')
48
54
  .then(({ data: snakey_schema }) => {
49
55
  const schema = humps_1.camelizeKeys(snakey_schema);
56
+ const serverActionQueue = function () {
57
+ const queue = [];
58
+ let inFlight = false;
59
+ return {
60
+ addItem: (item) => queue.push(item),
61
+ getItem: () => {
62
+ if (inFlight)
63
+ return false;
64
+ const item = queue.shift();
65
+ if (item) {
66
+ inFlight = true;
67
+ return item;
68
+ }
69
+ return false;
70
+ },
71
+ itemProcessed: () => inFlight = false,
72
+ fullySynced: () => queue.length === 0 && !inFlight,
73
+ getData: () => ({ queue, inFlight })
74
+ };
75
+ }();
50
76
  const consumer = actioncable_1.createConsumer();
51
77
  const allReducers = Object.assign(Object.assign({}, reducers), createJasonReducers_1.default(schema));
52
78
  console.log({ schema, allReducers });
@@ -78,7 +104,7 @@ const JasonProvider = ({ reducers, middleware, extraActions, children }) => {
78
104
  const md5Hash = blueimp_md5_1.default(JSON.stringify(config));
79
105
  console.log('Subscribe with', config, md5Hash);
80
106
  lodash_1.default.map(config, (v, model) => {
81
- payloadHandlers[`${model}:${md5Hash}`] = createPayloadHandler_1.default(store.dispatch, subscription, model, schema[model]);
107
+ payloadHandlers[`${model}:${md5Hash}`] = createPayloadHandler_1.default(store.dispatch, serverActionQueue, subscription, model, schema[model]);
82
108
  });
83
109
  subscription.send({ createSubscription: config });
84
110
  return () => removeSubscription(config);
@@ -90,7 +116,8 @@ const JasonProvider = ({ reducers, middleware, extraActions, children }) => {
90
116
  delete payloadHandlers[`${model}:${md5Hash}`];
91
117
  });
92
118
  }
93
- const actions = createActions_1.default(schema, store, restClient, extraActions);
119
+ const optDis = createOptDis_1.default(schema, store.dispatch, restClient, serverActionQueue);
120
+ const actions = createActions_1.default(schema, store, restClient, optDis, extraActions);
94
121
  const eager = makeEager_1.default(schema);
95
122
  console.log({ actions });
96
123
  setValue({
@@ -23,7 +23,7 @@ exports.default = (dis, store, entity, { extraActions = {}, hasPriority = false
23
23
  function remove(id) {
24
24
  return dis({ type: `${pluralize_1.default(entity)}/remove`, payload: id });
25
25
  }
26
- const extraActionsResolved = lodash_1.default.mapValues(extraActions, v => v(dis, store, entity));
26
+ const extraActionsResolved = extraActions ? lodash_1.default.mapValues(extraActions, v => v(dis, store, entity)) : {};
27
27
  if (hasPriority) {
28
28
  return Object.assign({ add, upsert, setAll, remove, movePriority }, extraActionsResolved);
29
29
  }
@@ -1,2 +1,2 @@
1
- declare function createActions(schema: any, store: any, restClient: any, extraActions: any): any;
1
+ declare function createActions(schema: any, store: any, restClient: any, optDis: any, extraActions: any): any;
2
2
  export default createActions;
@@ -6,32 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const actionFactory_1 = __importDefault(require("./actionFactory"));
7
7
  const pluralize_1 = __importDefault(require("pluralize"));
8
8
  const lodash_1 = __importDefault(require("lodash"));
9
- const uuid_1 = require("uuid");
10
- function enrich(type, payload) {
11
- if (type.split('/')[1] === 'upsert' && !(type.split('/')[0] === 'session')) {
12
- if (!payload.id) {
13
- return Object.assign(Object.assign({}, payload), { id: uuid_1.v4() });
14
- }
15
- }
16
- return payload;
17
- }
18
- function makeOptDis(schema, dispatch, restClient) {
19
- const plurals = lodash_1.default.keys(schema).map(k => pluralize_1.default(k));
20
- return function (action) {
21
- const { type, payload } = action;
22
- const data = enrich(type, payload);
23
- dispatch(action);
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
- function createActions(schema, store, restClient, extraActions) {
33
- const dis = store.dispatch;
34
- const optDis = makeOptDis(schema, dis, restClient);
9
+ function createActions(schema, store, restClient, optDis, extraActions) {
35
10
  const actions = lodash_1.default.fromPairs(lodash_1.default.map(schema, (config, model) => {
36
11
  if (config.priorityScope) {
37
12
  return [pluralize_1.default(model), actionFactory_1.default(optDis, store, model, { hasPriority: true })];
@@ -40,7 +15,7 @@ function createActions(schema, store, restClient, extraActions) {
40
15
  return [pluralize_1.default(model), actionFactory_1.default(optDis, store, model)];
41
16
  }
42
17
  }));
43
- const extraActionsResolved = extraActions(optDis, store, restClient);
18
+ const extraActionsResolved = extraActions ? extraActions(optDis, store, restClient, actions) : {};
44
19
  return lodash_1.default.merge(actions, extraActionsResolved);
45
20
  }
46
21
  exports.default = createActions;
@@ -19,6 +19,7 @@ function generateSlices(schema) {
19
19
  add: adapter.addOne,
20
20
  setAll: adapter.setAll,
21
21
  remove: adapter.removeOne,
22
+ removeMany: adapter.removeMany,
22
23
  movePriority: (s, { payload: { id, priority, parentFilter } }) => {
23
24
  // Get IDs and insert our item at the new index
24
25
  var affectedIds = lodash_1.default.orderBy(lodash_1.default.filter(lodash_1.default.values(s.entities), parentFilter).filter(e => e.id !== id), 'priority').map(e => e.id);
@@ -0,0 +1 @@
1
+ export default function createOptDis(schema: any, dispatch: any, restClient: any, serverActionQueue: any): (action: any) => void;
@@ -0,0 +1,45 @@
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
+ const pluralize_1 = __importDefault(require("pluralize"));
8
+ const uuid_1 = require("uuid");
9
+ function enrich(type, payload) {
10
+ if (type.split('/')[1] === 'upsert' && !(type.split('/')[0] === 'session')) {
11
+ if (!payload.id) {
12
+ return Object.assign(Object.assign({}, payload), { id: uuid_1.v4() });
13
+ }
14
+ }
15
+ return payload;
16
+ }
17
+ function createOptDis(schema, dispatch, restClient, serverActionQueue) {
18
+ const plurals = lodash_1.default.keys(schema).map(k => pluralize_1.default(k));
19
+ let inFlight = false;
20
+ function enqueueServerAction(action) {
21
+ serverActionQueue.addItem(action);
22
+ }
23
+ function dispatchServerAction() {
24
+ const action = serverActionQueue.getItem();
25
+ if (!action)
26
+ return;
27
+ inFlight = true;
28
+ restClient.post('/jason/api/action', action)
29
+ .then(serverActionQueue.itemProcessed)
30
+ .catch(e => {
31
+ dispatch({ type: 'upsertLocalUi', data: { error: JSON.stringify(e) } });
32
+ serverActionQueue.itemProcessed();
33
+ });
34
+ }
35
+ setInterval(dispatchServerAction, 10);
36
+ return function (action) {
37
+ const { type, payload } = action;
38
+ const data = enrich(type, payload);
39
+ dispatch({ type, payload: data });
40
+ if (plurals.indexOf(type.split('/')[0]) > -1) {
41
+ enqueueServerAction({ type, payload: data });
42
+ }
43
+ };
44
+ }
45
+ exports.default = createOptDis;
@@ -1 +1 @@
1
- export default function createPayloadHandler(dispatch: any, subscription: any, model: any, config: any): (data: any) => null | undefined;
1
+ export default function createPayloadHandler(dispatch: any, serverActionQueue: any, subscription: any, model: any, config: any): (data: any) => null | undefined;
@@ -4,16 +4,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const jsonpatch_1 = require("jsonpatch");
7
- const humps_1 = require("humps");
7
+ const deepCamelizeKeys_1 = __importDefault(require("./deepCamelizeKeys"));
8
8
  const pluralize_1 = __importDefault(require("pluralize"));
9
9
  const lodash_1 = __importDefault(require("lodash"));
10
+ const uuid_1 = require("uuid");
10
11
  function diffSeconds(dt2, dt1) {
11
12
  var diff = (dt2.getTime() - dt1.getTime()) / 1000;
12
13
  return Math.abs(Math.round(diff));
13
14
  }
14
- function createPayloadHandler(dispatch, subscription, model, config) {
15
+ function createPayloadHandler(dispatch, serverActionQueue, subscription, model, config) {
15
16
  console.log({ model, config });
16
- let payload = {};
17
+ let payload = [];
18
+ let previousPayload = [];
17
19
  let idx = 0;
18
20
  let patchQueue = {};
19
21
  let lastCheckAt = new Date();
@@ -23,16 +25,31 @@ function createPayloadHandler(dispatch, subscription, model, config) {
23
25
  console.log({ getPayload: model, subscription });
24
26
  subscription.send({ getPayload: { model, config } });
25
27
  }
28
+ function camelizeKeys(item) {
29
+ return deepCamelizeKeys_1.default(item, key => uuid_1.validate(key));
30
+ }
26
31
  const tGetPayload = lodash_1.default.throttle(getPayload, 10000);
27
32
  function dispatchPayload() {
33
+ // We want to avoid updates from server overwriting changes to local state, so if there is a queue then wait.
34
+ if (!serverActionQueue.fullySynced()) {
35
+ console.log(serverActionQueue.getData());
36
+ setTimeout(dispatchPayload, 100);
37
+ return;
38
+ }
28
39
  const includeModels = (config.includeModels || []).map(m => lodash_1.default.camelCase(m));
29
40
  console.log("Dispatching", { payload, includeModels });
30
41
  includeModels.forEach(m => {
31
- const subPayload = lodash_1.default.flatten(lodash_1.default.compact(humps_1.camelizeKeys(payload).map(instance => instance[m])));
32
- console.log({ type: `${pluralize_1.default(m)}/upsertMany`, payload: subPayload });
42
+ const subPayload = lodash_1.default.flatten(lodash_1.default.compact(camelizeKeys(payload).map(instance => instance[m])));
43
+ const previousSubPayload = lodash_1.default.flatten(lodash_1.default.compact(camelizeKeys(previousPayload).map(instance => instance[m])));
44
+ // Find IDs that were in the payload but are no longer
45
+ const idsToRemove = lodash_1.default.difference(previousSubPayload.map(i => i.id), subPayload.map(i => i.id));
33
46
  dispatch({ type: `${pluralize_1.default(m)}/upsertMany`, payload: subPayload });
47
+ dispatch({ type: `${pluralize_1.default(m)}/removeMany`, payload: idsToRemove });
34
48
  });
35
- dispatch({ type: `${pluralize_1.default(model)}/upsertMany`, payload: humps_1.camelizeKeys(payload) });
49
+ const idsToRemove = lodash_1.default.difference(previousPayload.map(i => i.id), payload.map(i => i.id));
50
+ dispatch({ type: `${pluralize_1.default(model)}/upsertMany`, payload: camelizeKeys(payload) });
51
+ dispatch({ type: `${pluralize_1.default(model)}/removeMany`, payload: idsToRemove });
52
+ previousPayload = payload;
36
53
  }
37
54
  function processQueue() {
38
55
  console.log({ idx, patchQueue });