proto.io 0.0.180 → 0.0.182

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 (32) hide show
  1. package/dist/adapters/file/database.d.ts +2 -2
  2. package/dist/adapters/file/filesystem.d.ts +2 -2
  3. package/dist/adapters/file/google-cloud-storage.d.ts +2 -2
  4. package/dist/adapters/storage/progres.d.ts +16 -35
  5. package/dist/adapters/storage/progres.js +31 -26
  6. package/dist/adapters/storage/progres.js.map +1 -1
  7. package/dist/adapters/storage/progres.mjs +31 -26
  8. package/dist/adapters/storage/progres.mjs.map +1 -1
  9. package/dist/client.d.ts +3 -3
  10. package/dist/client.js +1 -1
  11. package/dist/client.mjs +2 -2
  12. package/dist/index.d.ts +3 -3
  13. package/dist/index.js +244 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +245 -4
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/internals/{index-B7qz_VeO.d.ts → index-BZupzq1Y.d.ts} +2 -2
  18. package/dist/internals/index-BZupzq1Y.d.ts.map +1 -0
  19. package/dist/internals/{index-DwCNTJb5.js → index-BnDahEvz.js} +85 -1
  20. package/dist/internals/index-BnDahEvz.js.map +1 -0
  21. package/dist/internals/{index-CPodB1P0.mjs → index-CmJh3t_k.mjs} +85 -1
  22. package/dist/internals/index-CmJh3t_k.mjs.map +1 -0
  23. package/dist/internals/{index-BgZqiNxF.d.ts → index-YpB-hXxf.d.ts} +84 -1
  24. package/dist/internals/index-YpB-hXxf.d.ts.map +1 -0
  25. package/dist/internals/{index-CyXddWjz.d.ts → index-cKx59cIc.d.ts} +3 -2
  26. package/dist/internals/index-cKx59cIc.d.ts.map +1 -0
  27. package/package.json +1 -1
  28. package/dist/internals/index-B7qz_VeO.d.ts.map +0 -1
  29. package/dist/internals/index-BgZqiNxF.d.ts.map +0 -1
  30. package/dist/internals/index-CPodB1P0.mjs.map +0 -1
  31. package/dist/internals/index-CyXddWjz.d.ts.map +0 -1
  32. package/dist/internals/index-DwCNTJb5.js.map +0 -1
package/dist/index.mjs CHANGED
@@ -3,8 +3,8 @@ import { Server } from '@o2ter/server-js';
3
3
  import { Q as QueryValidator, r as resolveColumn, a as resolveDataType, g as generateId } from './internals/random-BSyWEK8G.mjs';
4
4
  import { P as PVK } from './internals/private-CNw40LZ7.mjs';
5
5
  import { asyncStream, prototypes, isBinaryData, base64ToBuffer } from '@o2ter/utils-js';
6
- import { T as TQuery, P as PROTO_NOTY_MSG, M as MASTER_USER_HEADER_NAME, a as MASTER_PASS_HEADER_NAME, A as AUTH_COOKIE_KEY, b as TUser, c as ProtoType, s as serialize, d as deserialize, U as UPLOAD_TOKEN_HEADER_NAME } from './internals/index-CPodB1P0.mjs';
7
- export { e as ProtoClient, f as classExtends, k as isFile, g as isObject, i as isQuery, j as isRole, h as isUser } from './internals/index-CPodB1P0.mjs';
6
+ import { T as TQuery, P as PROTO_NOTY_MSG, M as MASTER_USER_HEADER_NAME, a as MASTER_PASS_HEADER_NAME, A as AUTH_COOKIE_KEY, b as TUser, c as ProtoType, s as serialize, d as deserialize, U as UPLOAD_TOKEN_HEADER_NAME } from './internals/index-CmJh3t_k.mjs';
7
+ export { e as ProtoClient, f as classExtends, k as isFile, g as isObject, i as isQuery, j as isRole, h as isUser } from './internals/index-CmJh3t_k.mjs';
8
8
  import { i as isPointer, a as isRelation, T as TObject, b as isShape, d as defaultObjectKeyTypes, c as isPrimitive } from './internals/index-DyjcBbS1.mjs';
9
9
  import jwt from 'jsonwebtoken';
10
10
  import { Blob } from 'node:buffer';
@@ -520,6 +520,61 @@ const defaultSchema = {
520
520
  },
521
521
  ],
522
522
  },
523
+ '_Job': {
524
+ fields: {
525
+ name: 'string',
526
+ data: 'object',
527
+ error: 'object',
528
+ user: { type: 'pointer', target: 'User' },
529
+ startedAt: 'date',
530
+ completedAt: 'date',
531
+ locks: { type: 'relation', target: '_JobScope', foreignField: 'job' },
532
+ },
533
+ classLevelPermissions: {
534
+ find: [],
535
+ count: [],
536
+ create: [],
537
+ update: [],
538
+ delete: [],
539
+ },
540
+ fieldLevelPermissions: {
541
+ name: { update: [] },
542
+ data: { update: [] },
543
+ error: { update: [] },
544
+ user: { update: [] },
545
+ startedAt: { create: [], update: [] },
546
+ completedAt: { create: [], update: [] },
547
+ _expired_at: { create: [], update: [] },
548
+ },
549
+ indexes: [
550
+ { keys: { completedAt: 1, _created_at: 1 } },
551
+ ],
552
+ },
553
+ '_JobScope': {
554
+ fields: {
555
+ scope: 'string',
556
+ job: { type: 'pointer', target: '_Job' },
557
+ },
558
+ classLevelPermissions: {
559
+ get: [],
560
+ find: [],
561
+ count: [],
562
+ create: [],
563
+ update: [],
564
+ delete: [],
565
+ },
566
+ fieldLevelPermissions: {
567
+ scope: { update: [] },
568
+ job: { update: [] },
569
+ _expired_at: { create: [], update: [] },
570
+ },
571
+ indexes: [
572
+ {
573
+ keys: { scope: 1 },
574
+ unique: true,
575
+ },
576
+ ],
577
+ },
523
578
  };
524
579
 
525
580
  //
@@ -752,6 +807,8 @@ const mergeSchema = (...schemas) => _.reduce(schemas, (acc, schema) => ({
752
807
  class ProtoInternal {
753
808
  options;
754
809
  functions = {};
810
+ jobs = {};
811
+ jobRunner = new JobRunner();
755
812
  constructor(options) {
756
813
  validateSchemaName(options.schema);
757
814
  const schema = mergeSchema(defaultSchema, options.fileStorage.schema, options.schema);
@@ -770,6 +827,9 @@ class ProtoInternal {
770
827
  async prepare() {
771
828
  await this.options.storage.prepare(this.options.schema);
772
829
  }
830
+ shutdown() {
831
+ this.jobRunner.shutdown();
832
+ }
773
833
  generateId() {
774
834
  return generateId(this.options.objectIdSize);
775
835
  }
@@ -786,7 +846,7 @@ class ProtoInternal {
786
846
  return this.options.storage.setConfig(normalize(values), normalize(acl));
787
847
  }
788
848
  async run(proto, name, payload, options) {
789
- const func = this.functions?.[name];
849
+ const func = this.functions[name];
790
850
  if (_.isNil(func))
791
851
  throw Error('Function not found');
792
852
  if (_.isFunction(func))
@@ -1036,6 +1096,127 @@ class ProtoInternal {
1036
1096
  const storage = _serviceOf(options)?.storage ?? this.options.storage;
1037
1097
  return storage.refs(object, classNames, options?.master ? undefined : roles);
1038
1098
  }
1099
+ async scheduleJob(proto, name, params, options) {
1100
+ const opt = this.jobs[name];
1101
+ if (_.isNil(opt))
1102
+ throw Error('Job not found');
1103
+ const user = await proto.currentUser();
1104
+ if (!_.isFunction(opt)) {
1105
+ const roles = await proto.currentRoles();
1106
+ const { validator } = opt;
1107
+ if (!options?.master) {
1108
+ if (!!validator?.requireUser && !user)
1109
+ throw Error('No permission');
1110
+ if (!!validator?.requireMaster)
1111
+ throw Error('No permission');
1112
+ if (_.isArray(validator?.requireAnyUserRoles) && !_.some(validator?.requireAnyUserRoles, x => _.includes(roles, x)))
1113
+ throw Error('No permission');
1114
+ if (_.isArray(validator?.requireAllUserRoles) && _.some(validator?.requireAllUserRoles, x => !_.includes(roles, x)))
1115
+ throw Error('No permission');
1116
+ }
1117
+ }
1118
+ const obj = proto.Object('_Job');
1119
+ obj.set('name', name);
1120
+ obj.set('data', params);
1121
+ obj.set('user', user);
1122
+ await obj.save({ master: true });
1123
+ this.jobRunner.excuteJob(proto);
1124
+ return obj;
1125
+ }
1126
+ }
1127
+ class JobRunner {
1128
+ _running = false;
1129
+ _stopped = false;
1130
+ static TIMEOUT = 1000 * 60 * 5;
1131
+ static HEALTH = 1000 * 60;
1132
+ shutdown() {
1133
+ this._stopped = true;
1134
+ }
1135
+ async cleanUpOldJobs(proto) {
1136
+ await proto.Query('_JobScope').or(q => q.lessThan('_updated_at', new Date(Date.now() - JobRunner.TIMEOUT)), q => q.notEqualTo('job.completedAt', null)).deleteMany({ master: true });
1137
+ }
1138
+ async getAvailableJobs(proto) {
1139
+ const running = _.map(await proto.Query('_JobScope').find({ master: true }), x => x.get('scope'));
1140
+ const availableJobs = _.pickBy(proto[PVK].jobs, opt => {
1141
+ return _.isFunction(opt) || _.isEmpty(_.intersection(opt.scopes ?? [], running));
1142
+ });
1143
+ return _.keys(availableJobs);
1144
+ }
1145
+ async getNextJob(proto) {
1146
+ const availableJobs = await this.getAvailableJobs(proto);
1147
+ return await proto.Query('_Job')
1148
+ .containsIn('name', availableJobs)
1149
+ .or(q => q.lessThan('startedAt', new Date(Date.now() - JobRunner.TIMEOUT)), q => q.equalTo('startedAt', null))
1150
+ .equalTo('completedAt', null)
1151
+ .empty('locks')
1152
+ .includes('*', 'user')
1153
+ .sort({ _created_at: 1 })
1154
+ .first({ master: true });
1155
+ }
1156
+ async startJob(proto, job, opt) {
1157
+ await proto.withTransaction(async (session) => {
1158
+ for (const scope of _.isFunction(opt) ? [] : opt.scopes ?? []) {
1159
+ const obj = session.Object('_JobScope');
1160
+ obj.set('scope', scope);
1161
+ obj.set('job', job);
1162
+ await obj.save({ master: true });
1163
+ }
1164
+ job.set('startedAt', new Date());
1165
+ await job.save({ master: true, session });
1166
+ });
1167
+ }
1168
+ async updateJobScope(proto, job) {
1169
+ try {
1170
+ await proto.Query('_JobScope').equalTo('job', job).updateOne({}, { master: true });
1171
+ }
1172
+ catch (e) { }
1173
+ }
1174
+ async executeJobFunction(proto, job, opt) {
1175
+ const payload = Object.setPrototypeOf({ params: job.data, user: job.user, job }, this);
1176
+ const func = _.isFunction(opt) ? opt : opt.callback;
1177
+ await func(proxy(payload));
1178
+ }
1179
+ async finalizeJob(job, error = null) {
1180
+ if (error)
1181
+ job.set('error', _.pick(error, _.uniq(_.flatMap(prototypes(error), x => Object.getOwnPropertyNames(x)))));
1182
+ job.set('completedAt', new Date());
1183
+ await job.save({ master: true });
1184
+ }
1185
+ async excuteJob(proto) {
1186
+ if (this._running || this._stopped)
1187
+ return;
1188
+ this._running = true;
1189
+ while (!this._stopped) {
1190
+ await this.cleanUpOldJobs(proto);
1191
+ const job = await this.getNextJob(proto);
1192
+ if (!job)
1193
+ break;
1194
+ const opt = proto[PVK].jobs[job.name];
1195
+ if (_.isNil(opt))
1196
+ continue;
1197
+ try {
1198
+ await this.startJob(proto, job, opt);
1199
+ }
1200
+ catch (e) {
1201
+ continue;
1202
+ }
1203
+ (async () => {
1204
+ const timer = setInterval(() => this.updateJobScope(proto, job), JobRunner.HEALTH);
1205
+ try {
1206
+ await this.executeJobFunction(proto, job, opt);
1207
+ await this.finalizeJob(job);
1208
+ }
1209
+ catch (e) {
1210
+ await this.finalizeJob(job, e);
1211
+ }
1212
+ finally {
1213
+ clearInterval(timer);
1214
+ }
1215
+ this.excuteJob(proto);
1216
+ })();
1217
+ }
1218
+ this._running = false;
1219
+ }
1039
1220
  }
1040
1221
 
1041
1222
  //
@@ -1206,7 +1387,10 @@ const signUser = async (proto, res, user, options) => {
1206
1387
  const scheduleOp = {
1207
1388
  expireDocument: async (proto) => {
1208
1389
  await proto.gc();
1209
- }
1390
+ },
1391
+ excuteJob: async (proto) => {
1392
+ proto[PVK].jobRunner.excuteJob(proto);
1393
+ },
1210
1394
  };
1211
1395
  const schedule = (proto) => {
1212
1396
  let running = false;
@@ -1294,6 +1478,7 @@ class ProtoService extends ProtoType {
1294
1478
  }
1295
1479
  async shutdown() {
1296
1480
  this._schedule.destroy();
1481
+ this[PVK].shutdown();
1297
1482
  }
1298
1483
  classes() {
1299
1484
  return _.keys(this[PVK].options.schema);
@@ -1433,6 +1618,12 @@ class ProtoService extends ProtoType {
1433
1618
  define(name, callback, options) {
1434
1619
  this[PVK].functions[name] = options ? { callback, ...options } : callback;
1435
1620
  }
1621
+ scheduleJob(name, params, options) {
1622
+ return this[PVK].scheduleJob(this, name, params, options);
1623
+ }
1624
+ defineJob(name, callback, options) {
1625
+ this[PVK].jobs[name] = options ? { callback, ...options } : callback;
1626
+ }
1436
1627
  lockTable(className, update) {
1437
1628
  return this.storage.lockTable(className, update);
1438
1629
  }
@@ -1883,6 +2074,55 @@ var functionRoute = (router, proto) => {
1883
2074
  return router;
1884
2075
  };
1885
2076
 
2077
+ //
2078
+ // job.ts
2079
+ //
2080
+ // The MIT License
2081
+ // Copyright (c) 2021 - 2025 O2ter Limited. All rights reserved.
2082
+ //
2083
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
2084
+ // of this software and associated documentation files (the "Software"), to deal
2085
+ // in the Software without restriction, including without limitation the rights
2086
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2087
+ // copies of the Software, and to permit persons to whom the Software is
2088
+ // furnished to do so, subject to the following conditions:
2089
+ //
2090
+ // The above copyright notice and this permission notice shall be included in
2091
+ // all copies or substantial portions of the Software.
2092
+ //
2093
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2094
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2095
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2096
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2097
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2098
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2099
+ // THE SOFTWARE.
2100
+ //
2101
+ var jobRoute = (router, proto) => {
2102
+ router.get('/jobs/:name', async (req, res) => {
2103
+ res.setHeader('Cache-Control', ['no-cache', 'no-store']);
2104
+ const { name } = req.params;
2105
+ if (_.isNil(proto[PVK].jobs[name]))
2106
+ return void res.sendStatus(404);
2107
+ await response(res, () => {
2108
+ const payload = proto.connect(req);
2109
+ return payload[PVK].scheduleJob(payload, name, null, { master: payload.isMaster });
2110
+ });
2111
+ });
2112
+ router.post('/jobs/:name', Server.text({ type: '*/*' }), async (req, res) => {
2113
+ res.setHeader('Cache-Control', ['no-cache', 'no-store']);
2114
+ const { name } = req.params;
2115
+ if (_.isNil(proto[PVK].jobs[name]))
2116
+ return void res.sendStatus(404);
2117
+ await response(res, () => {
2118
+ const payload = proto.connect(req);
2119
+ const params = payload.rebind(deserialize(req.body, { objAttrs: TObject.defaultReadonlyKeys }));
2120
+ return payload[PVK].scheduleJob(payload, name, params, { master: payload.isMaster });
2121
+ });
2122
+ });
2123
+ return router;
2124
+ };
2125
+
1886
2126
  //
1887
2127
  // files.ts
1888
2128
  //
@@ -2277,6 +2517,7 @@ const ProtoRoute = async (options) => {
2277
2517
  });
2278
2518
  classesRoute(router, proto);
2279
2519
  functionRoute(router, proto);
2520
+ jobRoute(router, proto);
2280
2521
  filesRoute(router, proto);
2281
2522
  userRoute(router, proto);
2282
2523
  notifyRoute(router, proto);