ar_sync 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +27 -0
  3. data/Gemfile +1 -0
  4. data/Gemfile.lock +29 -31
  5. data/ar_sync.gemspec +1 -1
  6. data/core/{ActioncableAdapter.d.ts → ActionCableAdapter.d.ts} +0 -0
  7. data/core/ActionCableAdapter.js +31 -0
  8. data/core/ArSyncApi.d.ts +8 -2
  9. data/core/ArSyncApi.js +123 -49
  10. data/core/ArSyncModel.js +69 -60
  11. data/core/ArSyncStore.js +522 -381
  12. data/core/ConnectionManager.d.ts +1 -1
  13. data/core/ConnectionManager.js +45 -38
  14. data/core/DataType.d.ts +14 -9
  15. data/core/hooks.d.ts +5 -0
  16. data/core/hooks.js +64 -36
  17. data/gemfiles/Gemfile-rails-6 +9 -0
  18. data/gemfiles/Gemfile-rails-7 +9 -0
  19. data/index.js +2 -2
  20. data/lib/ar_sync/class_methods.rb +71 -36
  21. data/lib/ar_sync/collection.rb +23 -19
  22. data/lib/ar_sync/core.rb +3 -3
  23. data/lib/ar_sync/instance_methods.rb +7 -4
  24. data/lib/ar_sync/rails.rb +1 -1
  25. data/lib/ar_sync/type_script.rb +50 -14
  26. data/lib/ar_sync/version.rb +1 -1
  27. data/lib/generators/ar_sync/install/install_generator.rb +1 -1
  28. data/package-lock.json +1706 -227
  29. data/package.json +1 -1
  30. data/src/core/{ActioncableAdapter.ts → ActionCableAdapter.ts} +0 -0
  31. data/src/core/ArSyncApi.ts +20 -7
  32. data/src/core/ArSyncStore.ts +177 -125
  33. data/src/core/ConnectionManager.ts +1 -0
  34. data/src/core/DataType.ts +15 -16
  35. data/src/core/hooks.ts +31 -7
  36. data/tsconfig.json +2 -2
  37. data/vendor/assets/javascripts/{ar_sync_actioncable_adapter.js.erb → ar_sync_action_cable_adapter.js.erb} +1 -1
  38. metadata +17 -16
  39. data/core/ActioncableAdapter.js +0 -29
  40. data/lib/ar_sync/field.rb +0 -96
@@ -11,7 +11,7 @@ export default class ConnectionManager {
11
11
  unsubscribe: () => void;
12
12
  };
13
13
  subscribe(key: any, func: any): {
14
- unsubscribe: () => void;
14
+ unsubscribe(): void;
15
15
  };
16
16
  connect(key: any): any;
17
17
  disconnect(key: any): void;
@@ -1,75 +1,82 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- class ConnectionManager {
4
- constructor(adapter) {
3
+ var ConnectionManager = /** @class */ (function () {
4
+ function ConnectionManager(adapter) {
5
+ var _this = this;
5
6
  this.subscriptions = {};
6
7
  this.adapter = adapter;
7
8
  this.networkListeners = {};
8
9
  this.networkListenerSerial = 0;
9
10
  this.networkStatus = true;
10
- adapter.ondisconnect = () => {
11
- this.unsubscribeAll();
12
- this.triggerNetworkChange(false);
11
+ adapter.ondisconnect = function () {
12
+ _this.unsubscribeAll();
13
+ _this.triggerNetworkChange(false);
13
14
  };
14
- adapter.onreconnect = () => this.triggerNetworkChange(true);
15
+ adapter.onreconnect = function () { return _this.triggerNetworkChange(true); };
15
16
  }
16
- triggerNetworkChange(status) {
17
+ ConnectionManager.prototype.triggerNetworkChange = function (status) {
17
18
  if (this.networkStatus == status)
18
19
  return;
19
20
  this.networkStatus = status;
20
- for (const id in this.networkListeners)
21
+ for (var id in this.networkListeners)
21
22
  this.networkListeners[id](status);
22
- }
23
- unsubscribeAll() {
24
- for (const id in this.subscriptions) {
25
- const subscription = this.subscriptions[id];
23
+ };
24
+ ConnectionManager.prototype.unsubscribeAll = function () {
25
+ for (var id in this.subscriptions) {
26
+ var subscription = this.subscriptions[id];
26
27
  subscription.listeners = {};
27
28
  subscription.connection.unsubscribe();
28
29
  }
29
30
  this.subscriptions = {};
30
- }
31
- subscribeNetwork(func) {
32
- const id = this.networkListenerSerial++;
31
+ };
32
+ ConnectionManager.prototype.subscribeNetwork = function (func) {
33
+ var _this = this;
34
+ var id = this.networkListenerSerial++;
33
35
  this.networkListeners[id] = func;
34
- const unsubscribe = () => {
35
- delete this.networkListeners[id];
36
+ var unsubscribe = function () {
37
+ delete _this.networkListeners[id];
36
38
  };
37
- return { unsubscribe };
38
- }
39
- subscribe(key, func) {
40
- const subscription = this.connect(key);
41
- const id = subscription.serial++;
39
+ return { unsubscribe: unsubscribe };
40
+ };
41
+ ConnectionManager.prototype.subscribe = function (key, func) {
42
+ var _this = this;
43
+ if (!this.networkStatus)
44
+ return { unsubscribe: function () { } };
45
+ var subscription = this.connect(key);
46
+ var id = subscription.serial++;
42
47
  subscription.ref++;
43
48
  subscription.listeners[id] = func;
44
- const unsubscribe = () => {
49
+ var unsubscribe = function () {
45
50
  if (!subscription.listeners[id])
46
51
  return;
47
52
  delete subscription.listeners[id];
48
53
  subscription.ref--;
49
54
  if (subscription.ref === 0)
50
- this.disconnect(key);
55
+ _this.disconnect(key);
51
56
  };
52
- return { unsubscribe };
53
- }
54
- connect(key) {
57
+ return { unsubscribe: unsubscribe };
58
+ };
59
+ ConnectionManager.prototype.connect = function (key) {
60
+ var _this = this;
55
61
  if (this.subscriptions[key])
56
62
  return this.subscriptions[key];
57
- const connection = this.adapter.subscribe(key, data => this.received(key, data));
58
- return this.subscriptions[key] = { connection, listeners: {}, ref: 0, serial: 0 };
59
- }
60
- disconnect(key) {
61
- const subscription = this.subscriptions[key];
63
+ var connection = this.adapter.subscribe(key, function (data) { return _this.received(key, data); });
64
+ return this.subscriptions[key] = { connection: connection, listeners: {}, ref: 0, serial: 0 };
65
+ };
66
+ ConnectionManager.prototype.disconnect = function (key) {
67
+ var subscription = this.subscriptions[key];
62
68
  if (!subscription || subscription.ref !== 0)
63
69
  return;
64
70
  delete this.subscriptions[key];
65
71
  subscription.connection.unsubscribe();
66
- }
67
- received(key, data) {
68
- const subscription = this.subscriptions[key];
72
+ };
73
+ ConnectionManager.prototype.received = function (key, data) {
74
+ var subscription = this.subscriptions[key];
69
75
  if (!subscription)
70
76
  return;
71
- for (const id in subscription.listeners)
77
+ for (var id in subscription.listeners)
72
78
  subscription.listeners[id](data);
73
- }
74
- }
79
+ };
80
+ return ConnectionManager;
81
+ }());
75
82
  exports.default = ConnectionManager;
data/core/DataType.d.ts CHANGED
@@ -3,9 +3,7 @@ declare type RecordType = {
3
3
  query: any;
4
4
  };
5
5
  };
6
- declare type Values<T> = T extends {
7
- [K in keyof T]: infer U;
8
- } ? U : never;
6
+ declare type Values<T> = T[keyof T];
9
7
  declare type AddNullable<Test, Type> = null extends Test ? Type | null : Type;
10
8
  declare type DataTypeExtractField<BaseType, Key extends keyof BaseType> = Exclude<BaseType[Key], null> extends RecordType ? AddNullable<BaseType[Key], {}> : BaseType[Key] extends RecordType[] ? {}[] : BaseType[Key];
11
9
  declare type DataTypeExtractFieldsFromQuery<BaseType, Fields> = '*' extends Fields ? {
@@ -33,17 +31,24 @@ declare type CheckAttributesField<P, Q> = Q extends {
33
31
  declare type IsAnyCompareLeftType = {
34
32
  __any: never;
35
33
  };
36
- declare type CollectExtraFields<Type, Path> = IsAnyCompareLeftType extends Type ? null : Type extends ExtraFieldErrorType ? Path : Type extends (infer R)[] ? _CollectExtraFields<R> : _CollectExtraFields<Type>;
37
- declare type _CollectExtraFields<Type> = Type extends object ? (keyof (Type) extends never ? null : Values<{
38
- [key in keyof Type]: CollectExtraFields<Type[key], key>;
39
- }>) : null;
34
+ declare type CollectExtraFields<Type, Key> = IsAnyCompareLeftType extends Type ? never : Type extends ExtraFieldErrorType ? Key : Type extends (infer R)[] ? {
35
+ 0: Values<{
36
+ [key in keyof R]: CollectExtraFields<R[key], key>;
37
+ }>;
38
+ 1: never;
39
+ }[R extends object ? 0 : 1] : {
40
+ 0: Values<{
41
+ [key in keyof Type]: CollectExtraFields<Type[key], key>;
42
+ }>;
43
+ 1: never;
44
+ }[Type extends object ? 0 : 1];
40
45
  declare type SelectString<T> = T extends string ? T : never;
41
46
  declare type _ValidateDataTypeExtraFileds<Extra, Type> = SelectString<Extra> extends never ? Type : {
42
47
  error: {
43
- extraFields: SelectString<Extra>;
48
+ extraFields: Extra;
44
49
  };
45
50
  };
46
- declare type ValidateDataTypeExtraFileds<Type> = _ValidateDataTypeExtraFileds<CollectExtraFields<Type, []>, Type>;
51
+ declare type ValidateDataTypeExtraFileds<Type> = _ValidateDataTypeExtraFileds<CollectExtraFields<Type, never>, Type>;
47
52
  declare type RequestBase = {
48
53
  api: string;
49
54
  query: any;
data/core/hooks.d.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  declare let useState: <T>(t: T | (() => T)) => [T, (t: T | ((t: T) => T)) => void];
2
2
  declare let useEffect: (f: (() => void) | (() => (() => void)), deps: any[]) => void;
3
3
  declare let useMemo: <T>(f: () => T, deps: any[]) => T;
4
+ declare let useRef: <T>(value: T) => {
5
+ current: T;
6
+ };
4
7
  declare type InitializeHooksParams = {
5
8
  useState: typeof useState;
6
9
  useEffect: typeof useEffect;
7
10
  useMemo: typeof useMemo;
11
+ useRef: typeof useRef;
8
12
  };
9
13
  export declare function initializeHooks(hooks: InitializeHooksParams): void;
10
14
  interface ModelStatus {
@@ -16,6 +20,7 @@ export declare type DataAndStatus<T> = [T | null, ModelStatus];
16
20
  export interface Request {
17
21
  api: string;
18
22
  params?: any;
23
+ id?: number;
19
24
  query: any;
20
25
  }
21
26
  export declare function useArSyncModel<T>(request: Request | null): DataAndStatus<T>;
data/core/hooks.js CHANGED
@@ -1,37 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const ArSyncApi_1 = require("./ArSyncApi");
4
- const ArSyncModel_1 = require("./ArSyncModel");
5
- let useState;
6
- let useEffect;
7
- let useMemo;
3
+ exports.useArSyncFetch = exports.useArSyncModel = exports.initializeHooks = void 0;
4
+ var ArSyncApi_1 = require("./ArSyncApi");
5
+ var ArSyncModel_1 = require("./ArSyncModel");
6
+ var useState;
7
+ var useEffect;
8
+ var useMemo;
9
+ var useRef;
8
10
  function initializeHooks(hooks) {
9
11
  useState = hooks.useState;
10
12
  useEffect = hooks.useEffect;
11
13
  useMemo = hooks.useMemo;
14
+ useRef = hooks.useRef;
12
15
  }
13
16
  exports.initializeHooks = initializeHooks;
14
17
  function checkHooks() {
15
18
  if (!useState)
16
- throw 'uninitialized. needs `initializeHooks({ useState, useEffect, useMemo })`';
19
+ throw 'uninitialized. needs `initializeHooks({ useState, useEffect, useMemo, useRef })`';
17
20
  }
18
- const initialResult = [null, { complete: false, notfound: undefined, connected: true }];
21
+ var initialResult = [null, { complete: false, notfound: undefined, connected: true }];
19
22
  function useArSyncModel(request) {
23
+ var _a;
20
24
  checkHooks();
21
- const [result, setResult] = useState(initialResult);
22
- const requestString = JSON.stringify(request && request.params);
23
- useEffect(() => {
25
+ var _b = useState(initialResult), result = _b[0], setResult = _b[1];
26
+ var requestString = JSON.stringify((_a = request === null || request === void 0 ? void 0 : request.id) !== null && _a !== void 0 ? _a : request === null || request === void 0 ? void 0 : request.params);
27
+ var prevRequestStringRef = useRef(requestString);
28
+ useEffect(function () {
29
+ prevRequestStringRef.current = requestString;
24
30
  if (!request) {
25
31
  setResult(initialResult);
26
- return () => { };
32
+ return function () { };
27
33
  }
28
- const model = new ArSyncModel_1.default(request, { immutable: true });
34
+ var model = new ArSyncModel_1.default(request, { immutable: true });
29
35
  function update() {
30
- const { complete, notfound, connected, data } = model;
31
- setResult(resultWas => {
32
- const [, statusWas] = resultWas;
33
- const statusPersisted = statusWas.complete === complete && statusWas.notfound === notfound && statusWas.connected === connected;
34
- const status = statusPersisted ? statusWas : { complete, notfound, connected };
36
+ var complete = model.complete, notfound = model.notfound, connected = model.connected, data = model.data;
37
+ setResult(function (resultWas) {
38
+ var dataWas = resultWas[0], statusWas = resultWas[1];
39
+ var statusPersisted = statusWas.complete === complete && statusWas.notfound === notfound && statusWas.connected === connected;
40
+ if (dataWas === data && statusPersisted)
41
+ return resultWas;
42
+ var status = statusPersisted ? statusWas : { complete: complete, notfound: notfound, connected: connected };
35
43
  return [data, status];
36
44
  });
37
45
  }
@@ -41,21 +49,39 @@ function useArSyncModel(request) {
41
49
  else {
42
50
  setResult(initialResult);
43
51
  }
52
+ model.subscribe('load', update);
44
53
  model.subscribe('change', update);
45
54
  model.subscribe('connection', update);
46
- return () => model.release();
55
+ return function () { return model.release(); };
47
56
  }, [requestString]);
48
- return result;
57
+ return prevRequestStringRef.current === requestString ? result : initialResult;
49
58
  }
50
59
  exports.useArSyncModel = useArSyncModel;
51
- const initialFetchState = { data: null, status: { complete: false, notfound: undefined } };
60
+ var initialFetchState = { data: null, status: { complete: false, notfound: undefined } };
61
+ function extractParams(query, output) {
62
+ if (output === void 0) { output = []; }
63
+ if (typeof (query) !== 'object' || query == null || Array.isArray(query))
64
+ return output;
65
+ if ('params' in query)
66
+ output.push(query.params);
67
+ for (var key in query) {
68
+ extractParams(query[key], output);
69
+ }
70
+ return output;
71
+ }
52
72
  function useArSyncFetch(request) {
73
+ var _a;
53
74
  checkHooks();
54
- const [state, setState] = useState(initialFetchState);
55
- const requestString = JSON.stringify(request && request.params);
56
- const loader = useMemo(() => {
57
- let lastLoadId = 0;
58
- let timer = null;
75
+ var _b = useState(initialFetchState), state = _b[0], setState = _b[1];
76
+ var query = request && request.query;
77
+ var resourceIdentifier = (_a = request === null || request === void 0 ? void 0 : request.id) !== null && _a !== void 0 ? _a : request === null || request === void 0 ? void 0 : request.params;
78
+ var requestString = useMemo(function () {
79
+ return JSON.stringify(extractParams(query, [resourceIdentifier]));
80
+ }, [query, resourceIdentifier]);
81
+ var prevRequestStringRef = useRef(requestString);
82
+ var loader = useMemo(function () {
83
+ var lastLoadId = 0;
84
+ var timer = null;
59
85
  function cancel() {
60
86
  if (timer)
61
87
  clearTimeout(timer);
@@ -64,28 +90,28 @@ function useArSyncFetch(request) {
64
90
  }
65
91
  function fetch(request, retryCount) {
66
92
  cancel();
67
- const currentLoadingId = lastLoadId;
68
- ArSyncApi_1.default.fetch(request).then((response) => {
93
+ var currentLoadingId = lastLoadId;
94
+ ArSyncApi_1.default.fetch(request).then(function (response) {
69
95
  if (currentLoadingId !== lastLoadId)
70
96
  return;
71
97
  setState({ data: response, status: { complete: true, notfound: false } });
72
- }).catch(e => {
98
+ }).catch(function (e) {
73
99
  if (currentLoadingId !== lastLoadId)
74
100
  return;
75
101
  if (!e.retry) {
76
102
  setState({ data: null, status: { complete: true, notfound: true } });
77
103
  return;
78
104
  }
79
- timer = setTimeout(() => fetch(request, retryCount + 1), 1000 * Math.min(4 ** retryCount, 30));
105
+ timer = setTimeout(function () { return fetch(request, retryCount + 1); }, 1000 * Math.min(Math.pow(4, retryCount), 30));
80
106
  });
81
107
  }
82
108
  function update() {
83
109
  if (request) {
84
- setState(state => {
85
- const { data, status } = state;
110
+ setState(function (state) {
111
+ var data = state.data, status = state.status;
86
112
  if (!status.complete && status.notfound === undefined)
87
113
  return state;
88
- return { data, status: { complete: false, notfound: undefined } };
114
+ return { data: data, status: { complete: false, notfound: undefined } };
89
115
  });
90
116
  fetch(request, 0);
91
117
  }
@@ -93,13 +119,15 @@ function useArSyncFetch(request) {
93
119
  setState(initialFetchState);
94
120
  }
95
121
  }
96
- return { update, cancel };
122
+ return { update: update, cancel: cancel };
97
123
  }, [requestString]);
98
- useEffect(() => {
124
+ useEffect(function () {
125
+ prevRequestStringRef.current = requestString;
99
126
  setState(initialFetchState);
100
127
  loader.update();
101
- return () => loader.cancel();
128
+ return function () { return loader.cancel(); };
102
129
  }, [requestString]);
103
- return [state.data, state.status, loader.update];
130
+ var responseState = prevRequestStringRef.current === requestString ? state : initialFetchState;
131
+ return [responseState.data, responseState.status, loader.update];
104
132
  }
105
133
  exports.useArSyncFetch = useArSyncFetch;
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in ar_sync.gemspec
6
+ gemspec path: ".."
7
+
8
+ gem "activerecord", "~> 6.0.0"
9
+ gem 'ar_serializer', github: 'tompng/ar_serializer'
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in ar_sync.gemspec
6
+ gemspec path: ".."
7
+
8
+ gem "activerecord", "~> 7.0.0"
9
+ gem 'ar_serializer', github: 'tompng/ar_serializer'
data/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  var ArSyncModel_1 = require("./core/ArSyncModel");
4
- exports.ArSyncModel = ArSyncModel_1.default;
4
+ Object.defineProperty(exports, "ArSyncModel", { enumerable: true, get: function () { return ArSyncModel_1.default; } });
5
5
  var ArSyncApi_1 = require("./core/ArSyncApi");
6
- exports.ArSyncApi = ArSyncApi_1.default;
6
+ Object.defineProperty(exports, "ArSyncApi", { enumerable: true, get: function () { return ArSyncApi_1.default; } });
@@ -1,9 +1,10 @@
1
- require_relative 'field'
2
1
  require_relative 'collection'
3
2
 
4
3
  module ArSync::ModelBase::ClassMethods
5
4
  def _sync_self?
6
- instance_variable_defined? '@_sync_self'
5
+ return true if defined?(@_sync_self)
6
+
7
+ superclass._sync_self? if superclass < ActiveRecord::Base
7
8
  end
8
9
 
9
10
  def _sync_parents_info
@@ -21,7 +22,7 @@ module ArSync::ModelBase::ClassMethods
21
22
  end
22
23
 
23
24
  def _each_sync_parent(&block)
24
- _sync_parents_info.each(&block)
25
+ _sync_parents_info.each { |parent, options| block.call(parent, **options) }
25
26
  superclass._each_sync_parent(&block) if superclass < ActiveRecord::Base
26
27
  end
27
28
 
@@ -46,44 +47,73 @@ module ArSync::ModelBase::ClassMethods
46
47
  @_sync_self = true
47
48
  names.each do |name|
48
49
  _sync_children_info[name] = nil
49
- _sync_define name, option, &data_block
50
+ _sync_define name, **option, &data_block
50
51
  end
51
52
  end
52
53
 
53
54
  def sync_has_many(name, **option, &data_block)
54
55
  _sync_children_info[name] = [:many, option, data_block]
55
- _sync_has_many name, option, &data_block
56
+ _sync_has_many name, **option, &data_block
56
57
  end
57
58
 
58
59
  def sync_has_one(name, **option, &data_block)
59
60
  _sync_children_info[name] = [:one, option, data_block]
60
- _sync_define name, option, &data_block
61
+ _sync_define name, **option, &data_block
61
62
  end
62
63
 
63
- def _sync_has_many(name, order: :asc, limit: nil, preload: nil, association: nil, **option, &data_block)
64
- raise "order not in [:asc, :desc] : #{order}" unless %i[asc desc].include? order
64
+ def _sync_has_many(name, direction: :asc, first: nil, last: nil, preload: nil, association: nil, **option, &data_block)
65
+ raise ArgumentError, 'direction not in [:asc, :desc]' unless %i[asc desc].include? direction
66
+ raise ArgumentError, 'first and last cannot be both specified' if first && last
67
+ raise ArgumentError, 'cannot use first or last with direction: :desc' if direction != :asc && !first && !last
65
68
  if data_block.nil? && preload.nil?
66
- underscore_name = name.to_s.underscore.to_sym
67
- preload = lambda do |records, _context, params|
69
+ association_name = association || name.to_s.underscore.to_sym
70
+ order_option_from_params = lambda do |params|
71
+ if first || last
72
+ params_first = first && [first, params[:first]&.to_i].compact.min
73
+ params_last = last && [last, params[:last]&.to_i].compact.min
74
+ { direction: direction, first: params_first, last: params_last }
75
+ else
76
+ {
77
+ first: params[:first]&.to_i,
78
+ last: params[:last]&.to_i,
79
+ order_by: params[:order_by],
80
+ direction: params[:direction] || :asc
81
+ }
82
+ end
83
+ end
84
+ preload = lambda do |records, _context, **params|
68
85
  ArSerializer::Field.preload_association(
69
- self, records, association || underscore_name,
70
- order: (!limit && params && params[:order]) || order,
71
- limit: [params && params[:limit]&.to_i, limit].compact.min
86
+ self,
87
+ records,
88
+ association_name,
89
+ **order_option_from_params.call(params)
72
90
  )
73
91
  end
74
- data_block = lambda do |preloaded, _context, params|
75
- records = preloaded ? preloaded[id] || [] : send(name)
76
- next records unless limit || order == :asc
92
+ data_block = lambda do |preloaded, _context, **params|
93
+ records = preloaded ? preloaded[id] || [] : __send__(name)
94
+ next records unless first || last
77
95
  ArSync::CollectionWithOrder.new(
78
96
  records,
79
- order: (!limit && params && params[:order]) || order,
80
- limit: [params && params[:limit]&.to_i, limit].compact.min
97
+ **order_option_from_params.call(params)
81
98
  )
82
99
  end
83
- serializer_data_block = lambda do |preloaded, _context, _params|
84
- preloaded ? preloaded[id] || [] : send(name)
100
+ serializer_data_block = lambda do |preloaded, _context, **_params|
101
+ preloaded ? preloaded[id] || [] : __send__(name)
102
+ end
103
+ if first
104
+ params_type = { first?: :int }
105
+ elsif last
106
+ params_type = { last?: :int }
107
+ else
108
+ params_type = lambda do
109
+ orderable_keys = reflect_on_association(association_name)&.klass&._serializer_orderable_field_keys || []
110
+ orderable_keys &= [*option[:only]].map(&:to_s) if option[:only]
111
+ orderable_keys -= [*option[:except]].map(&:to_s) if option[:except]
112
+ orderable_keys |= ['id']
113
+ order_by = orderable_keys.size == 1 ? orderable_keys.first : orderable_keys.sort
114
+ { first?: :int, last?: :int, direction?: %w[asc desc], orderBy?: order_by }
115
+ end
85
116
  end
86
- params_type = { limit?: :int, order?: [{ :* => %w[asc desc] }, 'asc', 'desc'] }
87
117
  else
88
118
  params_type = {}
89
119
  end
@@ -104,18 +134,25 @@ module ArSync::ModelBase::ClassMethods
104
134
  serializer_field name, **option, namespace: :sync, &data_block
105
135
  end
106
136
 
107
- def sync_define_collection(name, limit: nil, order: :asc)
137
+ def sync_define_collection(name, first: nil, last: nil, direction: :asc)
108
138
  _initialize_sync_callbacks
109
- collection = ArSync::Collection.new self, name, limit: limit, order: order
139
+ collection = ArSync::Collection.new self, name, first: first, last: last, direction: direction
110
140
  sync_parent collection, inverse_of: [self, name]
111
141
  end
112
142
 
113
143
  module WriteHook
114
144
  def _initialize_sync_info_before_mutation
115
- self.class.default_scoped.scoping do
116
- @_sync_watch_values_before_mutation ||= _sync_current_watch_values
117
- @_sync_parents_info_before_mutation ||= _sync_current_parents_info
118
- @_sync_belongs_to_info_before_mutation ||= _sync_current_belongs_to_info
145
+ return unless defined? @_initialized
146
+ if new_record?
147
+ @_sync_watch_values_before_mutation ||= {}
148
+ @_sync_parents_info_before_mutation ||= {}
149
+ @_sync_belongs_to_info_before_mutation ||= {}
150
+ else
151
+ self.class.default_scoped.scoping do
152
+ @_sync_watch_values_before_mutation ||= _sync_current_watch_values
153
+ @_sync_parents_info_before_mutation ||= _sync_current_parents_info
154
+ @_sync_belongs_to_info_before_mutation ||= _sync_current_belongs_to_info
155
+ end
119
156
  end
120
157
  end
121
158
  def _write_attribute(attr_name, value)
@@ -129,7 +166,7 @@ module ArSync::ModelBase::ClassMethods
129
166
  end
130
167
 
131
168
  def _initialize_sync_callbacks
132
- return if instance_variable_defined? '@_sync_callbacks_initialized'
169
+ return if defined? @_sync_callbacks_initialized
133
170
  @_sync_callbacks_initialized = true
134
171
  prepend WriteHook
135
172
  attr_reader :_sync_parents_info_before_mutation, :_sync_belongs_to_info_before_mutation, :_sync_watch_values_before_mutation
@@ -140,8 +177,12 @@ module ArSync::ModelBase::ClassMethods
140
177
  ArSync.sync_keys self, current_user
141
178
  end
142
179
 
143
- _sync_define :defaults, namespace: :sync do |current_user|
144
- { sync_keys: ArSync.sync_keys(self, current_user) }
180
+ serializer_defaults namespace: :sync do |current_user|
181
+ { id: id, sync_keys: ArSync.sync_keys(self, current_user) }
182
+ end
183
+
184
+ after_initialize do
185
+ @_initialized = true
145
186
  end
146
187
 
147
188
  before_destroy do
@@ -150,12 +191,6 @@ module ArSync::ModelBase::ClassMethods
150
191
  @_sync_belongs_to_info_before_mutation ||= _sync_current_belongs_to_info
151
192
  end
152
193
 
153
- before_save on: :create do
154
- @_sync_parents_info_before_mutation ||= {}
155
- @_sync_watch_values_before_mutation ||= {}
156
- @_sync_belongs_to_info_before_mutation ||= {}
157
- end
158
-
159
194
  %i[create update destroy].each do |action|
160
195
  after_commit on: action do
161
196
  next if ArSync.skip_notification?
@@ -1,21 +1,26 @@
1
- require_relative 'field'
2
-
3
1
  class ArSync::Collection
4
- attr_reader :klass, :name, :limit, :order
5
- def initialize(klass, name, limit: nil, order: nil)
2
+ attr_reader :klass, :name, :first, :last, :direction, :ordering
3
+ def initialize(klass, name, first: nil, last: nil, direction: nil)
4
+ direction ||= :asc
6
5
  @klass = klass
7
6
  @name = name
8
- @limit = limit
9
- @order = order
7
+ @first = first
8
+ @last = last
9
+ @direction = direction
10
+ @ordering = { first: first, last: last, direction: direction }.compact
10
11
  self.class.defined_collections[[klass, name]] = self
11
12
  define_singleton_method(name) { to_a }
12
13
  end
13
14
 
14
15
  def to_a
15
- all = klass.all
16
- all = all.order id: order if order
17
- all = all.limit limit if limit
18
- all
16
+ if first
17
+ klass.order(id: direction).limit(first).to_a
18
+ elsif last
19
+ rev = direction == :asc ? :desc : :asc
20
+ klass.order(id: rev).limit(last).reverse
21
+ else
22
+ klass.all.to_a
23
+ end
19
24
  end
20
25
 
21
26
  def self.defined_collections
@@ -45,14 +50,13 @@ class ArSync::Collection
45
50
  end
46
51
  end
47
52
 
48
- class ArSync::CollectionWithOrder < ArSerializer::CompositeValue
49
- def initialize(records, order:, limit:)
50
- @records = records
51
- @order = { mode: order, limit: limit }
52
- end
53
-
54
- def ar_serializer_build_sub_calls
55
- values = @records.map { {} }
56
- [{ order: @order, collection: values }, @records.zip(values)]
53
+ class ArSync::CollectionWithOrder < ArSerializer::CustomSerializable
54
+ def initialize(records, direction:, first: nil, last: nil)
55
+ super records do |results|
56
+ {
57
+ ordering: { direction: direction || :asc, first: first, last: last }.compact,
58
+ collection: records.map(&results).compact
59
+ }
60
+ end
57
61
  end
58
62
  end
data/lib/ar_sync/core.rb CHANGED
@@ -82,17 +82,17 @@ module ArSync
82
82
  end
83
83
 
84
84
  def self.serialize(record_or_records, query, user: nil)
85
- ArSerializer.serialize record_or_records, query, context: user, include_id: true, use: :sync
85
+ ArSerializer.serialize record_or_records, query, context: user, use: :sync
86
86
  end
87
87
 
88
88
  def self.sync_serialize(target, user, query)
89
89
  case target
90
90
  when ArSync::Collection, ArSync::ModelBase
91
- serialized = ArSerializer.serialize target, query, context: user, include_id: true, use: :sync
91
+ serialized = ArSerializer.serialize target, query, context: user, use: :sync
92
92
  return serialized if target.is_a? ArSync::ModelBase
93
93
  {
94
94
  sync_keys: ArSync.sync_keys(target, user),
95
- order: { mode: target.order, limit: target.limit },
95
+ ordering: target.ordering,
96
96
  collection: serialized
97
97
  }
98
98
  when ActiveRecord::Relation, Array