backend-plus 2.7.0-beta.2 → 2.7.0-beta.4

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.
@@ -188,3 +188,14 @@ animation-time = 2s
188
188
  font-color gray
189
189
  margin-right 2px
190
190
  min-width 4em
191
+
192
+ .point
193
+ &::before
194
+ content '('
195
+ opacity 0.5
196
+ &::after
197
+ content ')'
198
+ opacity 0.5
199
+ .comma
200
+ opacity 0.5
201
+ padding-right 0.2em
@@ -169,7 +169,7 @@ export interface TableContext extends Context{
169
169
  superuser?:true
170
170
  forDump?:boolean
171
171
  }
172
- export type PgKnownTypes='decimal'|'text'|'boolean'|'integer'|'bigint'|'date'|'interval'|'timestamp'|'jsonb'|'double'|'bytea'|'jsona'|'time'|'tsrange'|'time_range'|'daterange'|'time_multirange';
172
+ export type PgKnownTypes='decimal'|'text'|'boolean'|'integer'|'bigint'|'date'|'interval'|'timestamp'|'jsonb'|'double'|'bytea'|'jsona'|'time'|'tsrange'|'time_range'|'daterange'|'time_multirange'|'gpoint'|'point';
173
173
  export type PgKnownDbValues='current_timestamp'|'current_user'|'session_user';
174
174
  export type SequenceDefinition = {
175
175
  name:string
@@ -73,7 +73,7 @@ if(dashDashDir>0){
73
73
  console.log('cwd',process.cwd());
74
74
  }
75
75
 
76
- /**
76
+ /**
77
77
  * @param {string} text
78
78
  * @return {string}
79
79
  */
@@ -81,14 +81,14 @@ function md5(text){
81
81
  return crypto.createHash('md5').update(text).digest('hex');
82
82
  }
83
83
 
84
- const DEFAULT_ITERATIONS = 4096;
84
+ const DEFAULT_ITERATIONS = 4096;
85
85
  const HASH_ALGORITHM = 'sha256';
86
86
 
87
87
  // primera version de scram sha 256. El Verifier completo tenía 64 bytes.
88
- const LEGACY_KEY_LENGTH = 64;
88
+ const LEGACY_KEY_LENGTH = 64;
89
89
 
90
90
  // Formato nuevo/PG-Compatible: El Client Key tiene 32 bytes (SHA-256).
91
- const SCRAM_KEY_LENGTH = 32;
91
+ const SCRAM_KEY_LENGTH = 32;
92
92
 
93
93
  const bufferToBase64 = (buffer) => buffer.toString('base64');
94
94
 
@@ -96,31 +96,31 @@ const bufferToBase64 = (buffer) => buffer.toString('base64');
96
96
  * Función central para PBKDF2 (Acepta KEY_LEN para compatibilidad).
97
97
  * Devuelve la clave derivada con la longitud especificada.
98
98
  */
99
- async function deriveKey(password, saltBuffer, iterations, keyLength) {
99
+ async function deriveKey(password, saltBuffer, iterations, keyLength) {
100
100
  return new Promise((resolve, reject) => {
101
101
  crypto.pbkdf2(
102
- password,
102
+ password,
103
103
  saltBuffer,
104
- iterations,
104
+ iterations,
105
105
  keyLength,
106
- HASH_ALGORITHM,
106
+ HASH_ALGORITHM,
107
107
  (err, derivedKey) => {
108
108
  if (err) return reject(err);
109
- resolve(derivedKey);
109
+ resolve(derivedKey);
110
110
  }
111
111
  );
112
112
  });
113
113
  }
114
114
 
115
115
  async function generateScramVerifier(password) {
116
- const saltBuffer = crypto.randomBytes(16);
116
+ const saltBuffer = crypto.randomBytes(16);
117
117
  const saltBase64 = bufferToBase64(saltBuffer);
118
118
 
119
119
  const Hi = await deriveKey(
120
- password,
121
- saltBuffer,
120
+ password,
121
+ saltBuffer,
122
122
  DEFAULT_ITERATIONS,
123
- SCRAM_KEY_LENGTH
123
+ SCRAM_KEY_LENGTH
124
124
  );
125
125
  const clientKey = crypto.createHmac('sha256', Hi).update('Client Key').digest();
126
126
  const serverKey = crypto.createHmac('sha256', Hi).update('Server Key').digest();
@@ -144,27 +144,27 @@ async function verifyScramPG(password, storedScramString) {
144
144
  if (!storedScramString.startsWith('SCRAM-SHA-256$')) {
145
145
  return false;
146
146
  }
147
-
147
+
148
148
  const parts = storedScramString.split('$');
149
149
  if (parts.length !== 3) {
150
150
  return false;
151
151
  }
152
-
152
+
153
153
  const [storedIterations, saltBase64] = parts[1].split(':');
154
154
  const [storedKeyBase64, serverKeyBase64] = parts[2].split(':'); // Asume StoredKey y ServerKey
155
155
 
156
156
  if (!saltBase64 || !storedKeyBase64 || !serverKeyBase64 || isNaN(parseInt(storedIterations))) {
157
157
  return false;
158
158
  }
159
-
159
+
160
160
  const iterations = parseInt(storedIterations);
161
161
  const saltBuffer = Buffer.from(saltBase64, 'base64');
162
162
  const storedKeyBuffer = Buffer.from(storedKeyBase64, 'base64');
163
163
  const serverKeyBuffer = Buffer.from(serverKeyBase64, 'base64');
164
164
 
165
165
  const Hi = await deriveKey(
166
- password,
167
- saltBuffer,
166
+ password,
167
+ saltBuffer,
168
168
  iterations,
169
169
  SCRAM_KEY_LENGTH
170
170
  );
@@ -203,21 +203,21 @@ async function verifyScramLegacy(password, storedScramString) {
203
203
  if (parts.length !== 3) {
204
204
  throw new Error('Formato SCRAM almacenado inválido.');
205
205
  }
206
-
206
+
207
207
  // Obtiene iteraciones y salt de la segunda parte (ej: '4096:SALT_BASE64')
208
208
  const [storedIterations, storedSalt] = parts[1].split(':');
209
209
  const storedVerifier = parts[2];
210
-
210
+
211
211
  if (!storedSalt || !storedVerifier || isNaN(parseInt(storedIterations))) {
212
212
  throw new Error('Datos de SCRAM incompletos o malformados.');
213
213
  }
214
-
214
+
215
215
  const iterations = parseInt(storedIterations);
216
216
 
217
217
  // Deriva la clave de la contraseña ingresada
218
218
  const generatedVerifierBuffer = await deriveKey(
219
- password,
220
- storedSalt,
219
+ password,
220
+ storedSalt,
221
221
  iterations,
222
222
  LEGACY_KEY_LENGTH
223
223
  );
@@ -313,7 +313,7 @@ AppBackend.prototype.configStaticConfig = function configStaticConfig(){
313
313
  min-version: 12
314
314
  fkOnUpdate: cascade
315
315
  max: 50
316
- log:
316
+ log:
317
317
  db:
318
318
  until: 2001-01-01 00:00
319
319
  last-error: false
@@ -326,7 +326,7 @@ AppBackend.prototype.configStaticConfig = function configStaticConfig(){
326
326
  "":
327
327
  local-path: for-client
328
328
  bin: {}
329
- client-setup:
329
+ client-setup:
330
330
  skin: ""
331
331
  lang: en
332
332
  version: 1.0
@@ -589,7 +589,7 @@ function MemoryPerodicallySaved(session){
589
589
  })}
590
590
 
591
591
  /**
592
- * @param {string} text
592
+ * @param {string} text
593
593
  */
594
594
  AppBackend.prototype.jsonPass = function jsonPass(text){
595
595
  return JSON.stringify(text,null,' ').replace(/\n(.*".*(pass|clave|secret).*":\s*").*(",?)\n/gi,'\n$1********$3\n');
@@ -751,7 +751,7 @@ AppBackend.prototype.start = function start(opts){
751
751
  while(iPosDbDump && iPosDbDump<process.argv.length && !process.argv[iPosDbDump].startsWith('--')){
752
752
  dumps.push(process.argv[iPosDbDump]);
753
753
  iPosDbDump++;
754
- }
754
+ }
755
755
  opts["dump-db"]={
756
756
  complete:!dumps.length,
757
757
  tableNames:dumps instanceof Array?dumps:null,
@@ -888,8 +888,8 @@ AppBackend.prototype.start = function start(opts){
888
888
  }
889
889
  be.db = pg;
890
890
  be.dbUserNameExpr="get_app_user()";
891
- be.dbUserRolExpr=`(select ${be.db.quoteIdent(be.config.login.rolFieldName)}
892
- from ${be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':''}${be.db.quoteIdent(be.config.login.table)}
891
+ be.dbUserRolExpr=`(select ${be.db.quoteIdent(be.config.login.rolFieldName)}
892
+ from ${be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':''}${be.db.quoteIdent(be.config.login.table)}
893
893
  where ${be.db.quoteIdent(be.config.login.userFieldName)} = ${be.dbUserNameExpr})`
894
894
  }).then(function(){
895
895
  if(opts["dump-db"]){
@@ -1032,6 +1032,7 @@ AppBackend.prototype.start = function start(opts){
1032
1032
  console.log(err);
1033
1033
  throw err;
1034
1034
  }
1035
+ await pg.setAllTypes(client);
1035
1036
  var data = await client.query("SELECT current_timestamp as cts").fetchUniqueRow();
1036
1037
  if(verboseStartup){
1037
1038
  console.log('NOW in Database',data.row.cts);
@@ -1047,7 +1048,7 @@ AppBackend.prototype.start = function start(opts){
1047
1048
  },function(){ /*OK, login.jade must not be here */ }),
1048
1049
  fs.stat('client/login.jade').then(function(){
1049
1050
  return Path.resolve(be.rootPath,'client/login');
1050
- },function(){
1051
+ },function(){
1051
1052
  return Path.join(__dirname,'../for-client/login');
1052
1053
  }).then(function(loginFile){
1053
1054
  be.config.login.plus.loginPageServe=function(req,res,next){
@@ -1164,7 +1165,7 @@ AppBackend.prototype.start = function start(opts){
1164
1165
  if(verboseStartup){
1165
1166
  console.log('-------------------');
1166
1167
  console.log(
1167
- 'be.config.login',
1168
+ 'be.config.login',
1168
1169
  be.jsonPass(be.config.login)
1169
1170
  );
1170
1171
  }
@@ -1193,7 +1194,7 @@ AppBackend.prototype.start = function start(opts){
1193
1194
  }
1194
1195
  });
1195
1196
  const updatePassword = async ({client, username, password, setUpdateDate, errorIfNoResult}) => {
1196
- const {table, passFieldName, userFieldName, passUpdatedAtFieldName, passAlgorithmFieldName, schema} = be.config.login;
1197
+ const {table, passFieldName, userFieldName, passUpdatedAtFieldName, passAlgorithmFieldName, schema} = be.config.login;
1197
1198
  const hashPass = await generateScramVerifier(password);
1198
1199
  let params = [username, hashPass];
1199
1200
  let setters = [`${be.db.quoteIdent(passFieldName)} = $2`];
@@ -1204,9 +1205,9 @@ AppBackend.prototype.start = function start(opts){
1204
1205
  if(setUpdateDate && passUpdatedAtFieldName){
1205
1206
  setters.push(`${be.db.quoteIdent(passUpdatedAtFieldName)} = current_timestamp`)
1206
1207
  }
1207
-
1208
+
1208
1209
  const result = await client.query(`
1209
- UPDATE ${(schema ? be.db.quoteIdent(schema) + '.' : '') + be.db.quoteIdent(table)}
1210
+ UPDATE ${(schema ? be.db.quoteIdent(schema) + '.' : '') + be.db.quoteIdent(table)}
1210
1211
  SET ${setters.join(', ')}
1211
1212
  WHERE ${be.db.quoteIdent(userFieldName)} = $1
1212
1213
  returning 1 as ok
@@ -1236,7 +1237,7 @@ AppBackend.prototype.start = function start(opts){
1236
1237
  [userFieldName]
1237
1238
  )
1238
1239
  ).concat(passFieldName);
1239
-
1240
+
1240
1241
  const sql = "SELECT "+infoFieldList.map(function(fieldOrPair){ return fieldOrPair.split(' as ').map(function(ident){ return be.db.quoteIdent(ident)}).join(' as '); })+
1241
1242
  ", "+be.config.login.activeClausule+" as active "+
1242
1243
  ", "+be.config.login.lockedClausule+" as locked "+
@@ -1264,7 +1265,7 @@ AppBackend.prototype.start = function start(opts){
1264
1265
  if(await verifyScramPG(password, user[passFieldName])){
1265
1266
  isScramValid = true;
1266
1267
  }
1267
-
1268
+
1268
1269
  // 2. Intento con formato Legacy (64 bytes)
1269
1270
  else if(await verifyScramLegacy(password, user[passFieldName])){
1270
1271
  isScramValid = true;
@@ -1274,7 +1275,7 @@ AppBackend.prototype.start = function start(opts){
1274
1275
  done(null,false,{message:be.messages.unlogged.login.userOrPassFail});
1275
1276
  return
1276
1277
  }
1277
- }else{
1278
+ }else{
1278
1279
  if (md5(password+username.toLowerCase()) === user[passFieldName]){
1279
1280
  needsMigration = true;
1280
1281
  }else{
@@ -1288,7 +1289,7 @@ AppBackend.prototype.start = function start(opts){
1288
1289
  if (be.config.log["pass-migration"]) console.log('Migración completada.');
1289
1290
  }
1290
1291
  }
1291
- //continua validando
1292
+ //continua validando
1292
1293
  if(data.rowCount==1){
1293
1294
  if(!data.row.active){
1294
1295
  done(null,false,{message:be.messages.unlogged.login.inactiveFail});
@@ -1344,7 +1345,7 @@ AppBackend.prototype.start = function start(opts){
1344
1345
  const data = await client.query(
1345
1346
  `SELECT *
1346
1347
  FROM ${(schema ? be.db.quoteIdent(schema) + '.' : '')}${be.db.quoteIdent(table)}
1347
- WHERE ${be.db.quoteIdent(userFieldName)} = $1`,
1348
+ WHERE ${be.db.quoteIdent(userFieldName)} = $1`,
1348
1349
  [username]
1349
1350
  ).fetchOneRowIfExists();
1350
1351
  if (data.rowCount !== 1) {
@@ -1360,7 +1361,7 @@ AppBackend.prototype.start = function start(opts){
1360
1361
  ok = await verifyScramLegacy(oldPassword, storedHash);
1361
1362
  }
1362
1363
  } else { //Intento MD5
1363
- const md5Hash = md5(oldPassword + username.toLowerCase());
1364
+ const md5Hash = md5(oldPassword + username.toLowerCase());
1364
1365
  ok = (md5Hash === storedHash);
1365
1366
  }
1366
1367
  if (!ok) {
@@ -1454,7 +1455,7 @@ AppBackend.prototype.start = function start(opts){
1454
1455
  to: be.config.mailer?.supervise?.to,
1455
1456
  subject: `npm start ${be.config["client-setup"]?.title || packagejson.name} ok ✔️`,
1456
1457
  text:`Inicio del servicio: ${new Date().toJSON()}
1457
-
1458
+
1458
1459
  Contexto: ${os.userInfo().username} ${process.cwd()}
1459
1460
  `
1460
1461
  }, {ignoreNoMailer:true, event:'restart-ok'})
@@ -1462,7 +1463,7 @@ AppBackend.prototype.start = function start(opts){
1462
1463
  if(err.dumping=='ok'){
1463
1464
  console.log('db struct dumped');
1464
1465
  process.exit(0);
1465
- return;
1466
+ return;
1466
1467
  }
1467
1468
  console.log('ERROR',err.stack || err);
1468
1469
  if(err.context){
@@ -1476,11 +1477,11 @@ AppBackend.prototype.start = function start(opts){
1476
1477
  to: be.config.mailer?.supervise?.to,
1477
1478
  subject: `npm start ${be.config["client-setup"]?.title || packagejson.name} fallido 🛑`,
1478
1479
  text:`Falla en el inicio del servicio: ${new Date().toJSON()}
1479
-
1480
+
1480
1481
  Contexto: ${os.userInfo().username} ${process.cwd()}
1481
1482
 
1482
1483
  Mensaje: ${err.message}
1483
-
1484
+
1484
1485
  ${err.stack}
1485
1486
  `
1486
1487
  }, {ignoreNoMailer:true, event:'restart-fail'})
@@ -1549,7 +1550,7 @@ AppBackend.prototype.checkDatabaseStructure = async function checkDatabaseStruct
1549
1550
  throw err;
1550
1551
  }
1551
1552
  try{
1552
- await client.query(`select ${be.db.quoteIdentList(be.config.login.forget.mailFields)}
1553
+ await client.query(`select ${be.db.quoteIdentList(be.config.login.forget.mailFields)}
1553
1554
  from ${be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':''}${be.config.login.table} limit 1`).fetchOneRowIfExists();
1554
1555
  }catch(err){
1555
1556
  var mensaje = `
@@ -1564,7 +1565,7 @@ AppBackend.prototype.checkDatabaseStructure = async function checkDatabaseStruct
1564
1565
  }
1565
1566
  }
1566
1567
  var bitacoraTableName = be.config.server.bitacoraTableName || 'bitacora';
1567
- var bitacoraId = await client.query(`SELECT data_type
1568
+ var bitacoraId = await client.query(`SELECT data_type
1568
1569
  FROM information_schema.columns
1569
1570
  WHERE /*table_schema = 'his' AND*/ table_name = '${bitacoraTableName}' AND column_name = 'id'
1570
1571
  `).fetchOneRowIfExists();
@@ -1609,9 +1610,9 @@ AppBackend.prototype.postConfig = function postConfig(){
1609
1610
  AppBackend.prototype.getContext = function getContext(req){
1610
1611
  var be = this;
1611
1612
  return {
1612
- be, user:req.user, session:req.session,
1613
- username:req.user && req.user[be.config.login.userFieldName],
1614
- machineId:req.machineId,
1613
+ be, user:req.user, session:req.session,
1614
+ username:req.user && req.user[be.config.login.userFieldName],
1615
+ machineId:req.machineId,
1615
1616
  navigator:(req.userAgent||{}).shortDescription||'?'
1616
1617
  };
1617
1618
  }
@@ -1641,7 +1642,7 @@ AppBackend.prototype.generateInsertSQL = function generateInsertSQL(schemaName,
1641
1642
  var cleanValues = [];
1642
1643
  for (var key in insertElement) {
1643
1644
  cleanKeys.push(db.quoteIdent(key));
1644
- cleanValues.push(db.quoteNullable(insertElement[key]));
1645
+ cleanValues.push(db.quoteNullable(insertElement[key]));
1645
1646
  }
1646
1647
  var sql = `INSERT INTO ${db.quoteIdent(schemaName)}.${db.quoteIdent(tableName)}
1647
1648
  (${cleanKeys.join(',')}) VALUES (${cleanValues.join(',')}) returning id`;
@@ -1659,7 +1660,7 @@ AppBackend.prototype.updateUpdateSQL = function updateUpdateSQL(schemaName, tabl
1659
1660
  for (var key in updateConditions) {
1660
1661
  filterPairs.push(be.db.quoteIdent(key) + " = " + be.db.quoteLiteral(updateConditions[key]));
1661
1662
  };
1662
- var sql = `UPDATE ${db.quoteIdent(schemaName)}.${db.quoteIdent(tableName)}
1663
+ var sql = `UPDATE ${db.quoteIdent(schemaName)}.${db.quoteIdent(tableName)}
1663
1664
  SET ${setPairs.join(',')}
1664
1665
  WHERE ${filterPairs.join(' AND ')}`;
1665
1666
  return sql;
@@ -1667,7 +1668,7 @@ AppBackend.prototype.updateUpdateSQL = function updateUpdateSQL(schemaName, tabl
1667
1668
 
1668
1669
 
1669
1670
  /** @param {boolean} forUnlogged */
1670
- AppBackend.prototype.addProcedureServices = function addProcedureServices(forUnlogged){
1671
+ AppBackend.prototype.addProcedureServices = function addProcedureServices(forUnlogged){
1671
1672
  var be = this;
1672
1673
  if(forUnlogged){
1673
1674
  var app = express();
@@ -1685,7 +1686,7 @@ AppBackend.prototype.addProcedureServices = function addProcedureServices(forUnl
1685
1686
  be.procedures = defs;
1686
1687
  be.clientSetup.procedure = be.procedure;
1687
1688
  app.get('/client-setup',async function(req, res, next){
1688
- if(forUnlogged && req.user){
1689
+ if(forUnlogged && req.user){
1689
1690
  // este pedido es para unlogged y está logueado, va al próximo
1690
1691
  next();
1691
1692
  }else{
@@ -1715,12 +1716,12 @@ AppBackend.prototype.addProcedureServices = function addProcedureServices(forUnl
1715
1716
  if(!isLowerIdent(procedureDef.action)){
1716
1717
  console.error('**** DEPRECATED ***** procedureDef action '+JSON.stringify(procedureDef.action)+' must be a Lower Ident');
1717
1718
  }
1718
- app[procedureDef.method]('/'+procedureDef.action,
1719
+ app[procedureDef.method]('/'+procedureDef.action,
1719
1720
  /**
1720
- *
1721
- * @param {Request} req
1722
- * @param {Response} res
1723
- * @param {import('express').NextFunction} next
1721
+ *
1722
+ * @param {Request} req
1723
+ * @param {Response} res
1724
+ * @param {import('express').NextFunction} next
1724
1725
  */
1725
1726
  async function(req, res, next){
1726
1727
  const BITACORA_SCHEMA = be.config.server.bitacoraSchema;
@@ -1773,9 +1774,9 @@ AppBackend.prototype.addProcedureServices = function addProcedureServices(forUnl
1773
1774
  init_date: initDatetimeString,
1774
1775
  };
1775
1776
  var getFinalStatusBitacoraElement = function getFinalStatusBitacoraElement(){
1776
- return {
1777
- end_date: getDatetimeString(),
1778
- end_status: status,
1777
+ return {
1778
+ end_date: getDatetimeString(),
1779
+ end_status: status,
1779
1780
  has_error: hasError
1780
1781
  }
1781
1782
  }
@@ -1805,12 +1806,12 @@ AppBackend.prototype.addProcedureServices = function addProcedureServices(forUnl
1805
1806
  var params = getParams();
1806
1807
  if(status){
1807
1808
  //terminó ejecucion
1808
- updateElement[procedureDef.bitacora.targetTableBitacoraFields.end_date] = getDatetimeString();
1809
+ updateElement[procedureDef.bitacora.targetTableBitacoraFields.end_date] = getDatetimeString();
1809
1810
  updateElement[procedureDef.bitacora.targetTableBitacoraFields.end_status] = status;
1810
1811
  updateElement[procedureDef.bitacora.targetTableBitacoraFields.has_error] = hasError;
1811
1812
  }else{
1812
1813
  //empezó ejecucion
1813
- updateElement[procedureDef.bitacora.targetTableBitacoraFields.init_date] = initDatetimeString;
1814
+ updateElement[procedureDef.bitacora.targetTableBitacoraFields.init_date] = initDatetimeString;
1814
1815
  }
1815
1816
  await be.inTransaction(req,async function(client){
1816
1817
  targetTableUpdateFieldsCondition.forEach(function(field){
@@ -2114,7 +2115,7 @@ AppBackend.prototype.optsGenericForFiles = function optsGenericForFiles(req, opt
2114
2115
  return changing(be.optsGenericForAll||{},{
2115
2116
  allowedExts:be.exts.normal,
2116
2117
  jade:{
2117
- skin:skin,
2118
+ skin:skin,
2118
2119
  skinUrl:skinUrl,
2119
2120
  formTitle:title,
2120
2121
  ...(opts?.withFlash ? {flash:req?.flash?.()} : {}),
@@ -2133,8 +2134,8 @@ AppBackend.prototype.unloggedLandPage = function unloggedLandPage(req){
2133
2134
  html.h2({style:'text-align:center; margin-top:15%; font-family:arial, sans-serif'},[be.messages.server.notLoggedIn]),
2134
2135
  html.h2({style:'text-align:center'},[html.code([
2135
2136
  html.a({
2136
- id:'goto-login',
2137
- style:'border:0.5px solid blue; border-radius: 6px; padding: 6px',
2137
+ id:'goto-login',
2138
+ style:'border:0.5px solid blue; border-radius: 6px; padding: 6px',
2138
2139
  href:Path.posix.join(be.config.server["base-url"],(be.config.login.plus.loginUrlPath||'/login'))
2139
2140
  },'login')
2140
2141
  ])]),
@@ -2157,8 +2158,8 @@ AppBackend.prototype.addUnloggedServices = function addUnloggedServices(mainApp,
2157
2158
  var now = bestGlobals.datetime.now();
2158
2159
  var token = crypto.randomUUID();
2159
2160
  var {rows} = await client.query(`select ${be.db.quoteIdent(be.config.login.userFieldName)} as username
2160
- from ${be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':''}${be.db.quoteIdent(be.config.login.table)}
2161
- where $1 in (${be.db.quoteIdentList(be.config.login.forget.mailFields)})
2161
+ from ${be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':''}${be.db.quoteIdent(be.config.login.table)}
2162
+ where $1 in (${be.db.quoteIdentList(be.config.login.forget.mailFields)})
2162
2163
  and ${be.config.login.activeClausule}
2163
2164
  and ${be.config.login.lockedClausule} is not true
2164
2165
  `, [req.body.email.toLowerCase()]).fetchAll();
@@ -2266,21 +2267,21 @@ AppBackend.prototype.addUnloggedServices = function addUnloggedServices(mainApp,
2266
2267
  // http://localhost:3033/img/login-logo-icon.png
2267
2268
  mainApp.get(Path.posix.join(baseUrl,'/img/login-logo-icon.png'), async function(req,res,next){
2268
2269
  var buscar = [
2269
- 'unlogged/img/login-logo-icon.svg',
2270
- 'dist/unlogged/img/login-logo-icon.svg',
2271
- 'dist/client/unlogged/img/login-logo-icon.svg',
2272
- 'unlogged/img/login-logo-icon.png',
2273
- 'dist/unlogged/img/login-logo-icon.png',
2274
- 'dist/client/unlogged/img/login-logo-icon.png',
2275
- 'unlogged/img/logo.png',
2276
- 'dist/unlogged/img/logo.png',
2277
- 'dist/client/unlogged/img/logo.png',
2278
- 'client/img/logo.png',
2279
- 'dist/client/img/logo.png',
2280
- 'dist/client/client/img/logo.png',
2270
+ 'unlogged/img/login-logo-icon.svg',
2271
+ 'dist/unlogged/img/login-logo-icon.svg',
2272
+ 'dist/client/unlogged/img/login-logo-icon.svg',
2273
+ 'unlogged/img/login-logo-icon.png',
2274
+ 'dist/unlogged/img/login-logo-icon.png',
2275
+ 'dist/client/unlogged/img/login-logo-icon.png',
2276
+ 'unlogged/img/logo.png',
2277
+ 'dist/unlogged/img/logo.png',
2278
+ 'dist/client/unlogged/img/logo.png',
2279
+ 'client/img/logo.png',
2280
+ 'dist/client/img/logo.png',
2281
+ 'dist/client/client/img/logo.png',
2281
2282
  'unlogged/img/logo-128.png',
2282
2283
  'dist/unlogged/img/logo-128.png',
2283
- 'dist/client/unlogged/img/logo-128.png'
2284
+ 'dist/client/unlogged/img/logo-128.png'
2284
2285
  ];
2285
2286
  buscar = buscar.map(n=>be.rootPath+'/'+n);
2286
2287
  buscar.push(__dirname+'/../for-client/img/login-logo-icon.png');
@@ -2295,7 +2296,7 @@ AppBackend.prototype.addUnloggedServices = function addUnloggedServices(mainApp,
2295
2296
  be.clientIncludesCompleted(null).filter(x => x.module).forEach(function (moduleDef) {
2296
2297
  if(baseUrl=='/'){
2297
2298
  baseUrl='';
2298
- }
2299
+ }
2299
2300
  let baseLib = baseUrl + '/' + (moduleDef.path ? moduleDef.path : be.esJavascript(moduleDef.type)? 'lib': 'css');
2300
2301
  resolve_module_dir(moduleDef.module, moduleDef.modPath, moduleDef.file ?? '.')
2301
2302
  try {
@@ -2319,7 +2320,7 @@ AppBackend.prototype.addUnloggedServices = function addUnloggedServices(mainApp,
2319
2320
  // ----------------------------------------------------
2320
2321
  var skin=be.config['client-setup'].skin;
2321
2322
  var skinUrl=(skin?skin+'/':'');
2322
- var optsGenericForFilesUnlogged=be.optsGenericForFiles();
2323
+ var optsGenericForFilesUnlogged=be.optsGenericForFiles();
2323
2324
  var skinPaths=[Path.join(be.rootPath,'skins')];
2324
2325
  if(be.config.server.skins[skin]['local-path']){
2325
2326
  skinPaths=skinPaths.concat(be.config.server.skins[skin]['local-path']).map(function(path){
@@ -2372,7 +2373,7 @@ AppBackend.prototype.getVisibleMenu = function getVisibleMenu(menu, context){
2372
2373
 
2373
2374
  AppBackend.prototype.clientIncludes = function clientIncludes(req, opts) {
2374
2375
  const hideBEPlusInclusions = opts === true || opts && typeof opts == "object" && opts.hideBEPlusInclusions;
2375
- opts = opts === true ? {} : opts || {};
2376
+ opts = opts === true ? {} : opts || {};
2376
2377
  var list = [];
2377
2378
  if (!hideBEPlusInclusions) {
2378
2379
  list = [
@@ -2463,7 +2464,7 @@ AppBackend.prototype.clientModules = function clientModules(req, opts) {
2463
2464
  }
2464
2465
 
2465
2466
  /**
2466
- * @param {string} tableName
2467
+ * @param {string} tableName
2467
2468
  * @param {(tableDef:typesOpe.TableDefinition, context?:TableContext)=>void} appenderFunction
2468
2469
  */
2469
2470
  AppBackend.prototype.appendToTableDefinition = function appendToTableDefinition(tableName, appenderFunction){
@@ -2520,16 +2521,16 @@ AppBackend.prototype.csss = function csss(hideBEPlusInclusions){
2520
2521
  AppBackend.prototype.scanSkinFiles = function scanSkinFiles() {
2521
2522
  var be = this;
2522
2523
  var skinName = be.config['client-setup'].skin;
2523
- be.activeSkinFiles = new Set();
2524
+ be.activeSkinFiles = new Set();
2524
2525
 
2525
2526
  var skinConfig = be.config.server.skins && be.config.server.skins[skinName];
2526
2527
 
2527
2528
  if (skinName && skinConfig && skinConfig['local-path']) {
2528
2529
  try {
2529
2530
  var skinPath = Path.join(Path.resolve(skinConfig['local-path']), skinName);
2530
-
2531
+
2531
2532
  if (fs.existsSync(skinPath)) {
2532
-
2533
+
2533
2534
  const walk = (currentPath) => {
2534
2535
  const entries = fs.readdirSync(currentPath, { withFileTypes: true });
2535
2536
 
@@ -2544,7 +2545,7 @@ AppBackend.prototype.scanSkinFiles = function scanSkinFiles() {
2544
2545
  var relativePath = Path.relative(skinPath, fullPath)
2545
2546
  .replace(/\\/g, '/')
2546
2547
  .replace(/^\//, '');
2547
-
2548
+
2548
2549
  be.activeSkinFiles.add(relativePath);
2549
2550
  }
2550
2551
  }
@@ -2631,7 +2632,7 @@ AppBackend.prototype.mainPage = function mainPage(req, offlineMode, opts){
2631
2632
 
2632
2633
  var lastDotIndex = css.lastIndexOf('.');
2633
2634
  var cssBase = (lastDotIndex !== -1) ? css.substring(0, lastDotIndex) : css;
2634
-
2635
+
2635
2636
  var existsInSkin = EXTENSIONES_SKIN.some(function(ext) {
2636
2637
  return be.activeSkinFiles.has(cssBase + ext);
2637
2638
  });
@@ -2897,7 +2898,7 @@ AppBackend.prototype.validateBitacora = function validateBitacora(procedureDef){
2897
2898
  var tableDefFields = be.tableStructures[procedureDef.bitacora.targetTable](contextForDump).fields;
2898
2899
  var targetTableBitacoraFields = procedureDef.bitacora.targetTableBitacoraFields;
2899
2900
  for (var fieldForSearch in targetTableBitacoraFields) {
2900
- var searchResult = tableDefFields.find(function findByName(field) {
2901
+ var searchResult = tableDefFields.find(function findByName(field) {
2901
2902
  return field.name === targetTableBitacoraFields[fieldForSearch];
2902
2903
  });
2903
2904
  if(!searchResult){
@@ -2905,12 +2906,12 @@ AppBackend.prototype.validateBitacora = function validateBitacora(procedureDef){
2905
2906
  }
2906
2907
  }
2907
2908
  var targetTableUpdateFieldsCondition = procedureDef.bitacora.targetTableUpdateFieldsCondition || ['init_date','end_date','has_error', 'end_status'];
2908
- if(targetTableUpdateFieldsCondition){
2909
+ if(targetTableUpdateFieldsCondition){
2909
2910
  if(targetTableUpdateFieldsCondition.length == 0){
2910
2911
  throw Error("Bitacora bad definition in core function '" + procedureDef.action + "', targetTableUpdateFieldsCondition must to be defined for table '" + procedureDef.bitacora.targetTable + "'.");
2911
2912
  }
2912
2913
  targetTableUpdateFieldsCondition.forEach(function(fieldName){
2913
- var searchResult = tableDefFields.find(function findByName(field) {
2914
+ var searchResult = tableDefFields.find(function findByName(field) {
2914
2915
  return field.name === fieldName;
2915
2916
  });
2916
2917
  if(!searchResult){
@@ -3040,9 +3041,9 @@ AppBackend.prototype.dumpDbTableFields = function dumpDbTableFields(tableDef, op
3040
3041
  (fieldDef.dataDecimals?','+fieldDef.dataDecimals:'')
3041
3042
  +')':fieldType)+
3042
3043
  ( be.specialSqlDefaultExpressions[fieldDef.defaultDbValue] != null ? ' default ' + be.specialSqlDefaultExpressions[fieldDef.defaultDbValue]
3043
- : fieldDef.defaultDbValue != null ? ' default ' + fieldDef.defaultDbValue
3044
+ : fieldDef.defaultDbValue != null ? ' default ' + fieldDef.defaultDbValue
3044
3045
  : be.specialSqlDefaultExpressions[fieldDef.specialDefaultValue] != null ? ' default ' + be.specialSqlDefaultExpressions[fieldDef.specialDefaultValue]
3045
- : fieldDef.defaultValue != null ? ' default ' + db.quoteLiteral(fieldDef.defaultValue)
3046
+ : fieldDef.defaultValue != null ? ' default ' + db.quoteLiteral(fieldDef.defaultValue)
3046
3047
  : ''
3047
3048
  ) +
3048
3049
  (be.isGeneratedSequence(fieldDef.sequence)?' generated always as identity':'')+
@@ -3195,7 +3196,7 @@ AppBackend.prototype.dumpDbSchemaPartial = async function dumpDbSchemaPartial(pa
3195
3196
  lines.push(');');
3196
3197
  //TODO: REFACTOR: Hacerlo mas sencillo
3197
3198
  // este codigo se encarga de convertir a rights de sql nuestros propios rights
3198
- // por ej (import -> [insert, update])
3199
+ // por ej (import -> [insert, update])
3199
3200
  var allows = tableDef.allow;
3200
3201
  var appToSqlRights = {'import': ['insert', 'update'], 'export': ['select'], 'deleteAll': ['delete']};
3201
3202
  [ 'import', 'export', 'deleteAll'].filter(function(right){
@@ -3252,7 +3253,7 @@ AppBackend.prototype.dumpDbSchemaPartial = async function dumpDbSchemaPartial(pa
3252
3253
  var prefix = 'alter table '+cualQuoteTableName+' add '+
3253
3254
  (cons.consName?'constraint '+db.quoteIdent(cons.consName)+' ':'');
3254
3255
  switch(cons.constraintType){
3255
- case 'unique':
3256
+ case 'unique':
3256
3257
  sql='('+cons.fields.map(function(field){ return db.quoteIdent(field); }).join(', ')+')';
3257
3258
  if(cons.where){
3258
3259
  if(cons.consName == null){
@@ -3264,10 +3265,10 @@ AppBackend.prototype.dumpDbSchemaPartial = async function dumpDbSchemaPartial(pa
3264
3265
  prefix += 'unique ';
3265
3266
  }
3266
3267
  break;
3267
- case 'check':
3268
+ case 'check':
3268
3269
  sql='check ('+cons.expr+')';
3269
3270
  break;
3270
- case 'exclude':
3271
+ case 'exclude':
3271
3272
  sql=`exclude using ${cons.using} (${cons.fields.map(f => typeof f == "string"?`${f} WITH =`:`${f.fieldName} WITH ${f.operator}`).join(', ')})${cons.where ? ` WHERE (${cons.where})`:``}`;
3272
3273
  break;
3273
3274
  default:
@@ -3310,7 +3311,7 @@ AppBackend.prototype.dumpDbSchemaPartial = async function dumpDbSchemaPartial(pa
3310
3311
  (polcom.using? ` USING ( ${polcom.using} )`:'')+
3311
3312
  (polcom.check?` WITH CHECK ( ${polcom.check} )`:'')+';'
3312
3313
  );
3313
- }
3314
+ }
3314
3315
  });
3315
3316
  }
3316
3317
  }else{
@@ -3351,7 +3352,7 @@ begin
3351
3352
  else
3352
3353
  select ${be.config.login.infoFieldList.map(fieldName => db.quoteIdent(fieldName)).join(', ')}
3353
3354
  into ${be.config.login.infoFieldList.map(fieldName => db.quoteIdent('v_'+fieldName)).join(', ')}
3354
-
3355
+
3355
3356
  from ${(be.config.login.from ?? (
3356
3357
  (be.config.login.schema?be.db.quoteIdent(be.config.login.schema)+'.':'')+
3357
3358
  be.db.quoteIdent(be.config.login.table)))}
@@ -3362,7 +3363,7 @@ begin
3362
3363
  set backend_plus._mode = normal;
3363
3364
  end if;
3364
3365
  perform set_config('backend_plus._user', p_username, false);
3365
- end;
3366
+ end;
3366
3367
  $body$;
3367
3368
 
3368
3369
  `)
@@ -3376,7 +3377,7 @@ $body$;
3376
3377
  var allTableContent = await fs.readFile('install/local-dump.psql','utf-8');
3377
3378
  var startIndex = allTableContent.indexOf('-- Data for Name: ');
3378
3379
  console.log('startIndex', startIndex);
3379
- var lastUseful = allTableContent.lastIndexOf('\nSELECT pg_catalog.setval')
3380
+ var lastUseful = allTableContent.lastIndexOf('\nSELECT pg_catalog.setval')
3380
3381
  console.log('lastUseful', lastUseful);
3381
3382
  if (lastUseful == -1) lastUseful = allTableContent.lastIndexOf('\n\\.\n');
3382
3383
  console.log('lastUseful', lastUseful);
@@ -3422,6 +3423,7 @@ $body$;
3422
3423
  return {path:Path.relative(process.cwd(),theTableFileName),content};
3423
3424
  });
3424
3425
  }).then(function({path,content}){
3426
+ /* LETRUA DEL ARCHIVO .TAB */
3425
3427
  dataText.push("\n-- table data: "+path);
3426
3428
  var lines;
3427
3429
  var rows;
@@ -3489,7 +3491,7 @@ $body$;
3489
3491
  throw Error("no se encuentra la columna "+filteredFieldDef[i]+" en "+tableName);
3490
3492
  }
3491
3493
  return value==='' ? (
3492
- def.allowEmptyText && ('nullable' in def) && !def.nullable ? "''" : 'null'
3494
+ def.allowEmptyText && ('nullable' in def) && !def.nullable ? "''" : 'null'
3493
3495
  ) : db.quoteNullable(value);
3494
3496
  }).join(', ')+")";
3495
3497
  }).join(',\n')+';\n';
@@ -3524,7 +3526,7 @@ $body$;
3524
3526
  ]
3525
3527
  .map(async function(fileNames){
3526
3528
  if (!fileNames) return '';
3527
- var i = 0;
3529
+ var i = 0;
3528
3530
  return (await Promise.all(fileNames.map(async fileName => {
3529
3531
  var content;
3530
3532
  do {
@@ -3576,7 +3578,7 @@ $body$;
3576
3578
  '\n-- functions\n' + functionLines.join('\n')+
3577
3579
  '\n-- lines \n' + lines.join('\n')+
3578
3580
  (complete? ('\n\n-- pre-ADAPTs\n'+texts[1]+'\n\n') : '' )+
3579
- (complete? ('\n\n-- DATA\n'+ dataText.join('\n')) : '' )+
3581
+ (complete? ('\n\n-- DATA\n'+ dataText.join('\n')) : '' )+
3580
3582
  (complete? ('\n\n-- ADAPTs\n'+ texts[2]+'\n\n') : '' )+
3581
3583
  '\n-- conss\n' + consLines.join('\n')+
3582
3584
  '\n-- FKs\n' + fkLines.join('\n')+
@@ -3606,14 +3608,17 @@ AppBackend.prototype.getDbFunctions = async function (){
3606
3608
  AppBackend.prototype.dumpDbSchema = async function dumpDbSchema(opts){
3607
3609
  var be = this;
3608
3610
  var {mainSql,enancePart} = await be.dumpDbSchemaPartial(
3609
- opts.complete?be.tableStructures:likeAr(be.tableStructures).filter((_, name)=>opts.tableNames.includes(name)),
3611
+ opts.complete?be.tableStructures:likeAr(be.tableStructures).filter((_, name)=>opts.tableNames.includes(name)),
3610
3612
  opts
3611
3613
  )
3612
3614
  mainSql=be.config.install.dump.db.extensions.map(function(extension){
3613
3615
  return ({
3614
- gist: "create extension if not exists btree_gist;",
3615
- pg_trgm: "create extension if not exists pg_trgm;",
3616
- pgcrypto: "create extension if not exists pgcrypto;"
3616
+ cube : "create extension if not exists cube ;",
3617
+ earthdistance : "create extension if not exists earthdistance ;",
3618
+ gist : "create extension if not exists btree_gist ;",
3619
+ pg_trgm : "create extension if not exists pg_trgm ;",
3620
+ pgcrypto : "create extension if not exists pgcrypto ;",
3621
+ postgis : "create extension if not exists postgis ;",
3617
3622
  }[extension]||('--unknown exension '+extension))+'\n';
3618
3623
  }).join('')+mainSql;
3619
3624
  await fs.writeFile('local-db-dump.sql', mainSql);
@@ -3686,7 +3691,7 @@ AppBackend.prototype.transformInput = function transformInput(fieldDef, value){
3686
3691
  return value;
3687
3692
  }
3688
3693
 
3689
- /**
3694
+ /**
3690
3695
  * xxxparam {{ action:string, parameters:any, conRegistro:boolean, conPadron:boolean, fileName?:string, csvFileName?:string, csvSeparator?:string, queries:{titulo:string, sql:string, params:string[]}[] }}
3691
3696
  * @param {{title:string, rows:Record<string, any>[]}[]} result
3692
3697
  * @returns {Promise<void>}
@@ -3784,7 +3789,7 @@ AppBackend.prototype.exportacionesGenerico = async function exportacionesGeneric
3784
3789
  ))
3785
3790
  }
3786
3791
  return [
3787
- ...(fileName?[{url:fileName, label:csvFileName?'xlsx de control':fileName}]:[]),
3792
+ ...(fileName?[{url:fileName, label:csvFileName?'xlsx de control':fileName}]:[]),
3788
3793
  ...(csvFileName?[{url:csvFileName, label:fileName?'csv (formato UTF-8)':csvFileName}]:[]),
3789
3794
  ];
3790
3795
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "backend-plus",
3
3
  "description": "Backend for the anti Pareto rule",
4
- "version": "2.7.0-beta.2",
4
+ "version": "2.7.0-beta.4",
5
5
  "author": "Codenautas <codenautas@googlegroups.com>",
6
6
  "license": "MIT",
7
7
  "repository": "codenautas/backend-plus",
@@ -46,9 +46,9 @@
46
46
  "ensure-login": "^0.1.6-beta.0",
47
47
  "express": "^5.2.1",
48
48
  "express-useragent": "^2.2.1",
49
- "fs-extra": "^11.3.6",
49
+ "fs-extra": "^11.4.0",
50
50
  "js-to-html": "^1.3.6",
51
- "js-yaml": "^5.2.1",
51
+ "js-yaml": "^5.2.2",
52
52
  "json4all": "^1.4.4",
53
53
  "lazy-some": "^0.1.0",
54
54
  "like-ar": "^0.5.3",
@@ -59,7 +59,7 @@
59
59
  "multiparty": "^4.3.0",
60
60
  "nodemailer": "^9.0.3",
61
61
  "numeral": "^2.0.6",
62
- "pg-promise-strict": "^1.4.6",
62
+ "pg-promise-strict": "^1.5.0",
63
63
  "pg-triggers": "0.4.6",
64
64
  "pikaday": "^1.8.2",
65
65
  "require-bro": "^0.3.6",
@@ -68,46 +68,47 @@
68
68
  "simple-git": "^3.36.0",
69
69
  "sql-tools": "^0.1.7",
70
70
  "stack-trace": "^1.0.0",
71
- "type-store": "^0.5.2",
72
- "typed-controls": "^0.12.6",
71
+ "tab-plus": "^0.1.7",
72
+ "type-store": "^0.6.0",
73
+ "typed-controls": "^0.12.7",
73
74
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
74
75
  },
75
76
  "devDependencies": {
76
77
  "@types/big.js": "^7.0.0",
77
- "@types/expect.js": "~0.3.32",
78
+ "@types/expect.js": "^0.3.32",
78
79
  "@types/express": "^5.0.6",
79
80
  "@types/express-useragent": "^1.0.5",
80
81
  "@types/fs-extra": "^11.0.4",
81
82
  "@types/js-yaml": "^4.0.9",
82
83
  "@types/mocha": "^10.0.10",
83
- "@types/multiparty": "~4.2.1",
84
- "@types/node": "^26.1.0",
84
+ "@types/multiparty": "^4.2.1",
85
+ "@types/node": "^26.1.1",
85
86
  "@types/nodemailer": "^8.0.1",
86
87
  "@types/numeral": "~2.0.5",
87
88
  "@types/session-file-store": "^1.2.6",
88
- "@types/stack-trace": "~0.0.33",
89
+ "@types/stack-trace": "^0.0.33",
89
90
  "@types/websql": "~0.0.30",
90
91
  "backend-skins": "^0.1.34",
91
92
  "discrepances": "^0.2.14",
92
93
  "esprima": "^4.0.1",
93
- "expect.js": "~0.3.1",
94
+ "expect.js": "^0.3.1",
94
95
  "karma": "6.4.4",
95
96
  "karma-chrome-launcher": "^3.2.0",
96
97
  "karma-expect": "^1.1.3",
97
98
  "karma-firefox-launcher": "^2.1.3",
98
99
  "karma-ie-launcher": "^1.0.0",
99
100
  "karma-mocha": "^2.0.1",
100
- "kill-9": "~0.4.3",
101
+ "kill-9": "^0.4.3",
101
102
  "mocha": "^11.7.6",
102
103
  "nyc": "^18.0.0",
103
104
  "puppeteer": "^25.3.0",
104
105
  "qa-control": "0.7.5",
105
106
  "regexplicit": "^0.1.3",
106
107
  "self-explain": "^0.11.0",
107
- "sinon": "^22.0.0",
108
+ "sinon": "^22.1.0",
108
109
  "supertest": "^7.2.2",
109
- "types.d.ts": "~0.6.22",
110
- "typescript": "^5.9.3",
110
+ "types.d.ts": "^0.6.22",
111
+ "typescript": "^5.9.2",
111
112
  "why-is-node-running": "^3.2.2"
112
113
  },
113
114
  "engines": {
@@ -118,7 +119,7 @@
118
119
  "test-ui": "(npm run prepublish || echo \"continue w/error\") && mocha --reporter spec --single-run --bail test/test-*.js",
119
120
  "test-karma": "(npm run prepublish || echo \"continue w/error\") && mocha --reporter spec --bail test/test-k*.js",
120
121
  "test-why": "node --expose-internals ./node_modules/mocha/bin/_mocha --reporter spec --bail test/test*.js",
121
- "test-ci": "npm test",
122
+ "test-ci": "mkdir -p coverage && > coverage/lcov.info && npm test",
122
123
  "test-cov": "(npm run prepublish || echo \"continue w/error\") && mocha --reporter spec --bail test/test*.js",
123
124
  "test-good": "mocha --reporter spec --bail --check-leaks test/test*.js",
124
125
  "example-pu": "node test/puppeteer/first-step.js",