jason-rails 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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 });