@nocobase/database 0.10.0-alpha.2 → 0.10.0-alpha.3

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.
package/lib/repository.js CHANGED
@@ -39,6 +39,10 @@ function _flat() {
39
39
  return data;
40
40
  }
41
41
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
42
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
43
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
44
+ function _iterableToArrayLimit(arr, i) { var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; if (null != _i) { var _s, _e, _x, _r, _arr = [], _n = !0, _d = !1; try { if (_x = (_i = _i.call(arr)).next, 0 === i) { if (Object(_i) !== _i) return; _n = !1; } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0); } catch (err) { _d = !0, _e = err; } finally { try { if (!_n && null != _i.return && (_r = _i.return(), Object(_r) !== _r)) return; } finally { if (_d) throw _e; } } return _arr; } }
45
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
42
46
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
43
47
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
44
48
  function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
@@ -46,7 +50,7 @@ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typ
46
50
  function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
47
51
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
48
52
  function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
49
- function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
53
+ function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }
50
54
  function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
51
55
  function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
52
56
  var __decorate = void 0 && (void 0).__decorate || function (decorators, target, key, desc) {
@@ -454,6 +458,43 @@ class Repository {
454
458
  }));
455
459
  const values = guard.sanitize(options.values);
456
460
  const queryOptions = _this10.buildQueryOptions(options);
461
+ // NOTE:
462
+ // 1. better to be moved to separated API like bulkUpdate/updateMany
463
+ // 2. strictly `false` comparing for compatibility of legacy api invoking
464
+ if (options.individualHooks === false) {
465
+ const Model = _this10.collection.model;
466
+ // @ts-ignore
467
+ const primaryKeyField = Model.primaryKeyField || Model.primaryKeyAttribute;
468
+ // NOTE:
469
+ // 1. find ids first for reusing `queryOptions` logic
470
+ // 2. estimation memory usage will be N * M bytes (N = rows, M = model object memory)
471
+ // 3. would be more efficient up to 100000 ~ 1000000 rows
472
+ const rows = yield Model.findAll(_objectSpread(_objectSpread({}, queryOptions), {}, {
473
+ attributes: [primaryKeyField],
474
+ group: `${Model.name}.${primaryKeyField}`,
475
+ include: queryOptions.include.filter(include => {
476
+ var _JSON$stringify3;
477
+ return Object.keys(include.where || {}).length > 0 || ((_JSON$stringify3 = JSON.stringify(queryOptions === null || queryOptions === void 0 ? void 0 : queryOptions.filter)) === null || _JSON$stringify3 === void 0 ? void 0 : _JSON$stringify3.includes(include.association));
478
+ }),
479
+ transaction
480
+ }));
481
+ const _yield$Model$update = yield Model.update(values, {
482
+ where: {
483
+ [primaryKeyField]: rows.map(row => row.get(primaryKeyField))
484
+ },
485
+ fields: options.fields,
486
+ hooks: options.hooks,
487
+ validate: options.validate,
488
+ sideEffects: options.sideEffects,
489
+ limit: options.limit,
490
+ silent: options.silent,
491
+ transaction
492
+ }),
493
+ _yield$Model$update2 = _slicedToArray(_yield$Model$update, 1),
494
+ result = _yield$Model$update2[0];
495
+ // TODO: not support association fields except belongsTo
496
+ return result;
497
+ }
457
498
  const instances = yield _this10.find(_objectSpread(_objectSpread({}, queryOptions), {}, {
458
499
  transaction
459
500
  }));
@@ -78,6 +78,7 @@ class UpdateGuard {
78
78
  // sanitize association values
79
79
  Object.keys(associationsValues).forEach(association => {
80
80
  let associationValues = associationsValues[association];
81
+ const associationObj = associations[association];
81
82
  const filterAssociationToBeUpdate = value => {
82
83
  if (value === null) {
83
84
  return value;
@@ -86,7 +87,6 @@ class UpdateGuard {
86
87
  if (associationKeysToBeUpdate.includes(association)) {
87
88
  return value;
88
89
  }
89
- const associationObj = associations[association];
90
90
  const associationKeyName = associationObj.associationType == 'BelongsTo' || associationObj.associationType == 'HasOne' ? associationObj.targetKey : associationObj.target.primaryKeyAttribute;
91
91
  if (value[associationKeyName]) {
92
92
  return _lodash().default.pick(value, [associationKeyName, ...Object.keys(associationObj.target.associations)]);
@@ -114,6 +114,15 @@ class UpdateGuard {
114
114
  }
115
115
  // set association values to sanitized value
116
116
  values[association] = associationValues;
117
+ if (associationObj.associationType === 'BelongsTo') {
118
+ if (typeof associationValues === 'object' && associationValues !== null) {
119
+ if (associationValues[associationObj.targetKey] != null) {
120
+ values[associationObj.foreignKey] = associationValues[associationObj.targetKey];
121
+ }
122
+ } else {
123
+ values[associationObj.foreignKey] = associationValues;
124
+ }
125
+ }
117
126
  });
118
127
  if (values instanceof _model.Model) {
119
128
  return values;
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@nocobase/database",
3
- "version": "0.10.0-alpha.2",
3
+ "version": "0.10.0-alpha.3",
4
4
  "description": "",
5
5
  "main": "./lib/index.js",
6
6
  "types": "./lib/index.d.ts",
7
7
  "license": "Apache-2.0",
8
8
  "dependencies": {
9
- "@nocobase/logger": "0.10.0-alpha.2",
10
- "@nocobase/utils": "0.10.0-alpha.2",
9
+ "@nocobase/logger": "0.10.0-alpha.3",
10
+ "@nocobase/utils": "0.10.0-alpha.3",
11
11
  "async-mutex": "^0.3.2",
12
12
  "cron-parser": "4.4.0",
13
13
  "deepmerge": "^4.2.2",
@@ -28,5 +28,5 @@
28
28
  "url": "git+https://github.com/nocobase/nocobase.git",
29
29
  "directory": "packages/database"
30
30
  },
31
- "gitHead": "85028ae1733fcbd46ecd5d291dacbdc175f7f073"
31
+ "gitHead": "1f0b27fc9ab2398cd41c308a6b01a986e025cd20"
32
32
  }
@@ -398,6 +398,7 @@ describe('repository.update', () => {
398
398
  fields: [
399
399
  { type: 'string', name: 'name' },
400
400
  { type: 'hasMany', name: 'comments' },
401
+ { type: 'belongsTo', name: 'user' },
401
402
  ],
402
403
  });
403
404
  Comment = db.collection({
@@ -411,7 +412,7 @@ describe('repository.update', () => {
411
412
  await db.close();
412
413
  });
413
414
 
414
- it('update1', async () => {
415
+ it('update with filterByTk and with associations', async () => {
415
416
  const user = await User.model.create<any>({
416
417
  name: 'user1',
417
418
  });
@@ -454,7 +455,7 @@ describe('repository.update', () => {
454
455
  expect(updated2.posts.length).toBe(2);
455
456
  });
456
457
 
457
- it('update2', async () => {
458
+ it('update with filterByTk', async () => {
458
459
  const user = await User.model.create<any>({
459
460
  name: 'user1',
460
461
  });
@@ -463,6 +464,9 @@ describe('repository.update', () => {
463
464
  name: 'user2',
464
465
  });
465
466
 
467
+ const hook = jest.fn();
468
+ db.on('users.afterUpdate', hook);
469
+
466
470
  await User.repository.update({
467
471
  filterByTk: user.id,
468
472
  values: {
@@ -470,6 +474,8 @@ describe('repository.update', () => {
470
474
  },
471
475
  });
472
476
 
477
+ expect(hook).toBeCalledTimes(1);
478
+
473
479
  const updated = await User.model.findByPk(user.id);
474
480
 
475
481
  expect(updated.get('name')).toEqual('user11');
@@ -477,6 +483,119 @@ describe('repository.update', () => {
477
483
  const u2 = await User.model.findByPk(user2.id);
478
484
  expect(u2.get('name')).toEqual('user2');
479
485
  });
486
+
487
+ it('update with filter one by one when individualHooks is not set', async () => {
488
+ const u1 = await User.repository.create({ values: { name: 'u1' } });
489
+
490
+ const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } });
491
+ const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } });
492
+ const p3 = await Post.repository.create({ values: { name: 'p3' } });
493
+
494
+ const hook = jest.fn();
495
+ db.on('posts.afterUpdate', hook);
496
+
497
+ await Post.repository.update({
498
+ filter: {
499
+ userId: u1.id,
500
+ },
501
+ values: {
502
+ name: 'pp',
503
+ },
504
+ });
505
+
506
+ const postsAfterUpdated = await Post.repository.find({ order: [['id', 'ASC']] });
507
+ expect(postsAfterUpdated[0].name).toBe('pp');
508
+ expect(postsAfterUpdated[1].name).toBe('pp');
509
+ expect(postsAfterUpdated[2].name).toBe('p3');
510
+
511
+ expect(hook).toBeCalledTimes(2);
512
+ });
513
+
514
+ it('update in batch when individualHooks is false', async () => {
515
+ const u1 = await User.repository.create({ values: { name: 'u1' } });
516
+
517
+ const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } });
518
+ const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } });
519
+ const p3 = await Post.repository.create({ values: { name: 'p3' } });
520
+
521
+ const hook = jest.fn();
522
+ db.on('posts.afterUpdate', hook);
523
+
524
+ await Post.repository.update({
525
+ filter: {
526
+ userId: u1.id,
527
+ },
528
+ values: {
529
+ name: 'pp',
530
+ },
531
+ individualHooks: false,
532
+ });
533
+
534
+ const postsAfterUpdated = await Post.repository.find({ order: [['id', 'ASC']] });
535
+ expect(postsAfterUpdated[0].name).toBe('pp');
536
+ expect(postsAfterUpdated[1].name).toBe('pp');
537
+ expect(postsAfterUpdated[2].name).toBe('p3');
538
+
539
+ expect(hook).toBeCalledTimes(0);
540
+ });
541
+
542
+ it('update in batch with belongsTo field as foreignKey', async () => {
543
+ const u1 = await User.repository.create({ values: { name: 'u1' } });
544
+ const u2 = await User.repository.create({ values: { name: 'u2' } });
545
+ const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } });
546
+ const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } });
547
+
548
+ const r1 = await Post.repository.update({
549
+ filter: {
550
+ name: p1.name,
551
+ },
552
+ values: {
553
+ user: u2.id,
554
+ },
555
+ individualHooks: false,
556
+ });
557
+
558
+ expect(r1).toEqual(1);
559
+
560
+ const p1Updated = await Post.repository.findOne({
561
+ filterByTk: p1.id,
562
+ });
563
+ expect(p1Updated.userId).toBe(u2.id);
564
+
565
+ const r2 = await Post.repository.update({
566
+ filter: {
567
+ id: p2.id,
568
+ },
569
+ values: {
570
+ user: null,
571
+ },
572
+ individualHooks: false,
573
+ });
574
+
575
+ expect(r2).toEqual(1);
576
+
577
+ const p2Updated = await Post.repository.findOne({
578
+ filterByTk: p2.id,
579
+ });
580
+ expect(p2Updated.userId).toBe(null);
581
+
582
+ const r3 = await Post.repository.update({
583
+ filter: {
584
+ id: p1.id,
585
+ },
586
+ values: {
587
+ user: { id: u1.id },
588
+ },
589
+ individualHooks: false,
590
+ });
591
+
592
+ expect(r3).toEqual(1);
593
+
594
+ const p1Updated2 = await Post.repository.findOne({
595
+ filterByTk: p1.id,
596
+ });
597
+ expect(p1Updated2.userId).toBe(u1.id);
598
+ });
480
599
  });
481
600
 
482
601
  describe('repository.destroy', () => {
@@ -370,6 +370,7 @@ describe('One2One Association', () => {
370
370
  uid: 1,
371
371
  name: '123',
372
372
  },
373
+ userId: 1,
373
374
  };
374
375
 
375
376
  const guard = new UpdateGuard();
@@ -381,6 +382,7 @@ describe('One2One Association', () => {
381
382
  user: {
382
383
  uid: 1,
383
384
  },
385
+ userId: 1,
384
386
  });
385
387
 
386
388
  guard.setAssociationKeysToBeUpdate(['user']);
package/src/repository.ts CHANGED
@@ -609,6 +609,45 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
609
609
 
610
610
  const queryOptions = this.buildQueryOptions(options);
611
611
 
612
+ // NOTE:
613
+ // 1. better to be moved to separated API like bulkUpdate/updateMany
614
+ // 2. strictly `false` comparing for compatibility of legacy api invoking
615
+ if (options.individualHooks === false) {
616
+ const { model: Model } = this.collection;
617
+ // @ts-ignore
618
+ const primaryKeyField = Model.primaryKeyField || Model.primaryKeyAttribute;
619
+ // NOTE:
620
+ // 1. find ids first for reusing `queryOptions` logic
621
+ // 2. estimation memory usage will be N * M bytes (N = rows, M = model object memory)
622
+ // 3. would be more efficient up to 100000 ~ 1000000 rows
623
+ const rows = await Model.findAll({
624
+ ...queryOptions,
625
+ attributes: [primaryKeyField],
626
+ group: `${Model.name}.${primaryKeyField}`,
627
+ include: queryOptions.include.filter((include) => {
628
+ return (
629
+ Object.keys(include.where || {}).length > 0 ||
630
+ JSON.stringify(queryOptions?.filter)?.includes(include.association)
631
+ );
632
+ }),
633
+ transaction,
634
+ });
635
+ const [result] = await Model.update(values, {
636
+ where: {
637
+ [primaryKeyField]: rows.map((row) => row.get(primaryKeyField)),
638
+ },
639
+ fields: options.fields,
640
+ hooks: options.hooks,
641
+ validate: options.validate,
642
+ sideEffects: options.sideEffects,
643
+ limit: options.limit,
644
+ silent: options.silent,
645
+ transaction,
646
+ });
647
+ // TODO: not support association fields except belongsTo
648
+ return result;
649
+ }
650
+
612
651
  const instances = await this.find({
613
652
  ...queryOptions,
614
653
  transaction,
@@ -93,6 +93,8 @@ export class UpdateGuard {
93
93
  Object.keys(associationsValues).forEach((association) => {
94
94
  let associationValues = associationsValues[association];
95
95
 
96
+ const associationObj = associations[association];
97
+
96
98
  const filterAssociationToBeUpdate = (value) => {
97
99
  if (value === null) {
98
100
  return value;
@@ -104,8 +106,6 @@ export class UpdateGuard {
104
106
  return value;
105
107
  }
106
108
 
107
- const associationObj = associations[association];
108
-
109
109
  const associationKeyName =
110
110
  associationObj.associationType == 'BelongsTo' || associationObj.associationType == 'HasOne'
111
111
  ? (<any>associationObj).targetKey
@@ -143,6 +143,16 @@ export class UpdateGuard {
143
143
 
144
144
  // set association values to sanitized value
145
145
  values[association] = associationValues;
146
+
147
+ if (associationObj.associationType === 'BelongsTo') {
148
+ if (typeof associationValues === 'object' && associationValues !== null) {
149
+ if (associationValues[(associationObj as any).targetKey] != null) {
150
+ values[(associationObj as any).foreignKey] = associationValues[(associationObj as any).targetKey];
151
+ }
152
+ } else {
153
+ values[(associationObj as any).foreignKey] = associationValues;
154
+ }
155
+ }
146
156
  });
147
157
 
148
158
  if (values instanceof Model) {