@nymphjs/driver-sqlite3 1.0.0-beta.109 → 1.0.0-beta.110

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.
@@ -1048,7 +1048,7 @@ export default class SQLite3Driver extends NymphDriver {
1048
1048
  for (const etype of etypes) {
1049
1049
  // Export entities.
1050
1050
  const dataIterator: IterableIterator<any> = this.queryArray(
1051
- `SELECT e.*, d."name", d."value", json(d."json") as "json", d."string", d."number" FROM ${SQLite3Driver.escape(
1051
+ `SELECT e."guid", e."tags", e."cdate", e."mdate", e."user", e."group", e."acUser", e."acGroup", e."acOther", e."acRead", e."acWrite", e."acFull", d."name", d."value", json(d."json") as "json", d."string", d."number" FROM ${SQLite3Driver.escape(
1052
1052
  `${this.prefix}entities_${etype}`,
1053
1053
  )} e LEFT JOIN ${SQLite3Driver.escape(
1054
1054
  `${this.prefix}data_${etype}`,
@@ -1060,10 +1060,48 @@ export default class SQLite3Driver extends NymphDriver {
1060
1060
  const tags = datum.value.tags.slice(1, -1);
1061
1061
  const cdate = datum.value.cdate;
1062
1062
  const mdate = datum.value.mdate;
1063
+ const user = datum.value.user;
1064
+ const group = datum.value.group;
1065
+ const acUser = datum.value.acUser;
1066
+ const acGroup = datum.value.acGroup;
1067
+ const acOther = datum.value.acOther;
1068
+ const acRead = datum.value.acRead?.slice(1, -1).split(',');
1069
+ const acWrite = datum.value.acWrite?.slice(1, -1).split(',');
1070
+ const acFull = datum.value.acFull?.slice(1, -1).split(',');
1063
1071
  let currentEntityExport: string[] = [];
1064
1072
  currentEntityExport.push(`{${guid}}<${etype}>[${tags}]`);
1065
1073
  currentEntityExport.push(`\tcdate=${JSON.stringify(cdate)}`);
1066
1074
  currentEntityExport.push(`\tmdate=${JSON.stringify(mdate)}`);
1075
+ if (this.nymph.tilmeld != null) {
1076
+ if (user != null) {
1077
+ currentEntityExport.push(
1078
+ `\tuser=${JSON.stringify(['nymph_entity_reference', user, 'User'])}`,
1079
+ );
1080
+ }
1081
+ if (group != null) {
1082
+ currentEntityExport.push(
1083
+ `\tgroup=${JSON.stringify(['nymph_entity_reference', group, 'Group'])}`,
1084
+ );
1085
+ }
1086
+ if (acUser != null) {
1087
+ currentEntityExport.push(`\tacUser=${JSON.stringify(acUser)}`);
1088
+ }
1089
+ if (acGroup != null) {
1090
+ currentEntityExport.push(`\tacGroup=${JSON.stringify(acGroup)}`);
1091
+ }
1092
+ if (acOther != null) {
1093
+ currentEntityExport.push(`\tacOther=${JSON.stringify(acOther)}`);
1094
+ }
1095
+ if (acRead != null) {
1096
+ currentEntityExport.push(`\tacRead=${JSON.stringify(acRead)}`);
1097
+ }
1098
+ if (acWrite != null) {
1099
+ currentEntityExport.push(`\tacWrite=${JSON.stringify(acWrite)}`);
1100
+ }
1101
+ if (acFull != null) {
1102
+ currentEntityExport.push(`\tacFull=${JSON.stringify(acFull)}`);
1103
+ }
1104
+ }
1067
1105
  if (datum.value.name != null) {
1068
1106
  // This do will keep going and adding the data until the
1069
1107
  // next entity is reached. datum will end on the next entity.
@@ -1172,17 +1210,39 @@ export default class SQLite3Driver extends NymphDriver {
1172
1210
  if (curQuery) {
1173
1211
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1174
1212
  }
1175
- const name = `param${++count.i}`;
1176
- curQuery +=
1177
- ieTable +
1178
- '."guid" ' +
1179
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1180
- 'IN (SELECT "guid" FROM ' +
1181
- SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1182
- ' WHERE "name"=@' +
1183
- name +
1184
- ')';
1185
- params[name] = curVar;
1213
+ if (
1214
+ curVar === 'cdate' ||
1215
+ curVar === 'mdate' ||
1216
+ (this.nymph.tilmeld != null &&
1217
+ (curVar === 'user' ||
1218
+ curVar === 'group' ||
1219
+ curVar === 'acUser' ||
1220
+ curVar === 'acGroup' ||
1221
+ curVar === 'acOther' ||
1222
+ curVar === 'acRead' ||
1223
+ curVar === 'acWrite' ||
1224
+ curVar === 'acFull'))
1225
+ ) {
1226
+ curQuery +=
1227
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1228
+ '(' +
1229
+ ieTable +
1230
+ '.' +
1231
+ SQLite3Driver.escape(curVar) +
1232
+ ' IS NOT NULL)';
1233
+ } else {
1234
+ const name = `param${++count.i}`;
1235
+ curQuery +=
1236
+ ieTable +
1237
+ '."guid" ' +
1238
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1239
+ 'IN (SELECT "guid" FROM ' +
1240
+ SQLite3Driver.escape(this.prefix + 'data_' + etype) +
1241
+ ' WHERE "name"=@' +
1242
+ name +
1243
+ ')';
1244
+ params[name] = curVar;
1245
+ }
1186
1246
  }
1187
1247
  break;
1188
1248
  case 'truthy':
@@ -1191,20 +1251,26 @@ export default class SQLite3Driver extends NymphDriver {
1191
1251
  if (curQuery) {
1192
1252
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1193
1253
  }
1194
- if (curVar === 'cdate') {
1195
- curQuery +=
1196
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1197
- '(' +
1198
- ieTable +
1199
- '."cdate" NOT NULL)';
1200
- break;
1201
- } else if (curVar === 'mdate') {
1254
+ if (
1255
+ curVar === 'cdate' ||
1256
+ curVar === 'mdate' ||
1257
+ (this.nymph.tilmeld != null &&
1258
+ (curVar === 'user' ||
1259
+ curVar === 'group' ||
1260
+ curVar === 'acUser' ||
1261
+ curVar === 'acGroup' ||
1262
+ curVar === 'acOther' ||
1263
+ curVar === 'acRead' ||
1264
+ curVar === 'acWrite' ||
1265
+ curVar === 'acFull'))
1266
+ ) {
1202
1267
  curQuery +=
1203
1268
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1204
1269
  '(' +
1205
1270
  ieTable +
1206
- '."mdate" NOT NULL)';
1207
- break;
1271
+ '.' +
1272
+ SQLite3Driver.escape(curVar) +
1273
+ ' IS NOT NULL)';
1208
1274
  } else {
1209
1275
  const name = `param${++count.i}`;
1210
1276
  curQuery +=
@@ -1222,30 +1288,62 @@ export default class SQLite3Driver extends NymphDriver {
1222
1288
  break;
1223
1289
  case 'equal':
1224
1290
  case '!equal':
1225
- if (curValue[0] === 'cdate') {
1291
+ if (
1292
+ curValue[0] === 'cdate' ||
1293
+ curValue[0] === 'mdate' ||
1294
+ (this.nymph.tilmeld != null &&
1295
+ (curValue[0] === 'acUser' ||
1296
+ curValue[0] === 'acGroup' ||
1297
+ curValue[0] === 'acOther'))
1298
+ ) {
1299
+ if (curQuery) {
1300
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1301
+ }
1302
+ const value = `param${++count.i}`;
1303
+ curQuery +=
1304
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1305
+ ieTable +
1306
+ '.' +
1307
+ SQLite3Driver.escape(curValue[0]) +
1308
+ '=@' +
1309
+ value;
1310
+ params[value] = Number(curValue[1]);
1311
+ } else if (
1312
+ this.nymph.tilmeld != null &&
1313
+ (curValue[0] === 'user' || curValue[0] === 'group')
1314
+ ) {
1226
1315
  if (curQuery) {
1227
1316
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1228
1317
  }
1229
- const cdate = `param${++count.i}`;
1318
+ const value = `param${++count.i}`;
1230
1319
  curQuery +=
1231
1320
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1232
1321
  ieTable +
1233
- '."cdate"=@' +
1234
- cdate;
1235
- params[cdate] = Number(curValue[1]);
1236
- break;
1237
- } else if (curValue[0] === 'mdate') {
1322
+ '.' +
1323
+ SQLite3Driver.escape(curValue[0]) +
1324
+ '=@' +
1325
+ value;
1326
+ params[value] = `${curValue[1]}`;
1327
+ } else if (
1328
+ this.nymph.tilmeld != null &&
1329
+ (curValue[0] === 'acRead' ||
1330
+ curValue[0] === 'acWrite' ||
1331
+ curValue[0] === 'acFull')
1332
+ ) {
1238
1333
  if (curQuery) {
1239
1334
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1240
1335
  }
1241
- const mdate = `param${++count.i}`;
1336
+ const value = `param${++count.i}`;
1242
1337
  curQuery +=
1243
1338
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1244
1339
  ieTable +
1245
- '."mdate"=@' +
1246
- mdate;
1247
- params[mdate] = Number(curValue[1]);
1248
- break;
1340
+ '.' +
1341
+ SQLite3Driver.escape(curValue[0]) +
1342
+ '=@' +
1343
+ value;
1344
+ params[value] = Array.isArray(curValue[1])
1345
+ ? ',' + curValue[1].join(',') + ','
1346
+ : '';
1249
1347
  } else if (typeof curValue[1] === 'number') {
1250
1348
  if (curQuery) {
1251
1349
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1316,30 +1414,67 @@ export default class SQLite3Driver extends NymphDriver {
1316
1414
  break;
1317
1415
  case 'contain':
1318
1416
  case '!contain':
1319
- if (curValue[0] === 'cdate') {
1417
+ if (
1418
+ curValue[0] === 'cdate' ||
1419
+ curValue[0] === 'mdate' ||
1420
+ (this.nymph.tilmeld != null &&
1421
+ (curValue[0] === 'acUser' ||
1422
+ curValue[0] === 'acGroup' ||
1423
+ curValue[0] === 'acOther'))
1424
+ ) {
1425
+ if (curQuery) {
1426
+ curQuery += typeIsOr ? ' OR ' : ' AND ';
1427
+ }
1428
+ const value = `param${++count.i}`;
1429
+ curQuery +=
1430
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1431
+ ieTable +
1432
+ '.' +
1433
+ SQLite3Driver.escape(curValue[0]) +
1434
+ '=@' +
1435
+ value;
1436
+ params[value] = Number(curValue[1]);
1437
+ } else if (
1438
+ this.nymph.tilmeld != null &&
1439
+ (curValue[0] === 'user' || curValue[0] === 'group')
1440
+ ) {
1320
1441
  if (curQuery) {
1321
1442
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1322
1443
  }
1323
- const cdate = `param${++count.i}`;
1444
+ const value = `param${++count.i}`;
1324
1445
  curQuery +=
1325
1446
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1326
1447
  ieTable +
1327
- '."cdate"=@' +
1328
- cdate;
1329
- params[cdate] = Number(curValue[1]);
1330
- break;
1331
- } else if (curValue[0] === 'mdate') {
1448
+ '.' +
1449
+ SQLite3Driver.escape(curValue[0]) +
1450
+ '=@' +
1451
+ value;
1452
+ params[value] = `${curValue[1]}`;
1453
+ } else if (
1454
+ this.nymph.tilmeld != null &&
1455
+ (curValue[0] === 'acRead' ||
1456
+ curValue[0] === 'acWrite' ||
1457
+ curValue[0] === 'acFull')
1458
+ ) {
1332
1459
  if (curQuery) {
1333
1460
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1334
1461
  }
1335
- const mdate = `param${++count.i}`;
1462
+ const id = `param${++count.i}`;
1336
1463
  curQuery +=
1337
1464
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1338
1465
  ieTable +
1339
- '."mdate"=@' +
1340
- mdate;
1341
- params[mdate] = Number(curValue[1]);
1342
- break;
1466
+ '.' +
1467
+ SQLite3Driver.escape(curValue[0]) +
1468
+ ' LIKE @' +
1469
+ id +
1470
+ " ESCAPE '\\'";
1471
+ params[id] =
1472
+ '%,' +
1473
+ curValue[1]
1474
+ .replace('\\', '\\\\')
1475
+ .replace('%', '\\%')
1476
+ .replace('_', '\\_') +
1477
+ ',%';
1343
1478
  } else {
1344
1479
  const containTableSuffix = makeTableSuffix();
1345
1480
  if (curQuery) {
@@ -1377,12 +1512,23 @@ export default class SQLite3Driver extends NymphDriver {
1377
1512
  break;
1378
1513
  case 'search':
1379
1514
  case '!search':
1380
- if (curValue[0] === 'cdate' || curValue[0] === 'mdate') {
1515
+ if (
1516
+ curValue[0] === 'cdate' ||
1517
+ curValue[0] === 'mdate' ||
1518
+ (this.nymph.tilmeld != null &&
1519
+ (curValue[0] === 'user' ||
1520
+ curValue[0] === 'group' ||
1521
+ curValue[0] === 'acUser' ||
1522
+ curValue[0] === 'acGroup' ||
1523
+ curValue[0] === 'acOther' ||
1524
+ curValue[0] === 'acRead' ||
1525
+ curValue[0] === 'acWrite' ||
1526
+ curValue[0] === 'acFull'))
1527
+ ) {
1381
1528
  if (curQuery) {
1382
1529
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1383
1530
  }
1384
1531
  curQuery += (xor(typeIsNot, clauseNot) ? 'NOT ' : '') + '(0)';
1385
- break;
1386
1532
  } else {
1387
1533
  if (curQuery) {
1388
1534
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1522,34 +1668,33 @@ export default class SQLite3Driver extends NymphDriver {
1522
1668
  break;
1523
1669
  case 'match':
1524
1670
  case '!match':
1525
- if (curValue[0] === 'cdate') {
1526
- if (curQuery) {
1527
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1528
- }
1529
- const cdate = `param${++count.i}`;
1530
- curQuery +=
1531
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1532
- '(' +
1533
- ieTable +
1534
- '."cdate" REGEXP @' +
1535
- cdate +
1536
- ')';
1537
- params[cdate] = curValue[1];
1538
- break;
1539
- } else if (curValue[0] === 'mdate') {
1671
+ if (
1672
+ curValue[0] === 'cdate' ||
1673
+ curValue[0] === 'mdate' ||
1674
+ (this.nymph.tilmeld != null &&
1675
+ (curValue[0] === 'user' ||
1676
+ curValue[0] === 'group' ||
1677
+ curValue[0] === 'acUser' ||
1678
+ curValue[0] === 'acGroup' ||
1679
+ curValue[0] === 'acOther' ||
1680
+ curValue[0] === 'acRead' ||
1681
+ curValue[0] === 'acWrite' ||
1682
+ curValue[0] === 'acFull'))
1683
+ ) {
1540
1684
  if (curQuery) {
1541
1685
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1542
1686
  }
1543
- const mdate = `param${++count.i}`;
1687
+ const value = `param${++count.i}`;
1544
1688
  curQuery +=
1545
1689
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1546
1690
  '(' +
1547
1691
  ieTable +
1548
- '."mdate" REGEXP @' +
1549
- mdate +
1692
+ '.' +
1693
+ SQLite3Driver.escape(curValue[0]) +
1694
+ ' REGEXP @' +
1695
+ value +
1550
1696
  ')';
1551
- params[mdate] = curValue[1];
1552
- break;
1697
+ params[value] = curValue[1];
1553
1698
  } else {
1554
1699
  if (curQuery) {
1555
1700
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1573,34 +1718,33 @@ export default class SQLite3Driver extends NymphDriver {
1573
1718
  break;
1574
1719
  case 'imatch':
1575
1720
  case '!imatch':
1576
- if (curValue[0] === 'cdate') {
1577
- if (curQuery) {
1578
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1579
- }
1580
- const cdate = `param${++count.i}`;
1581
- curQuery +=
1582
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1583
- '(' +
1584
- ieTable +
1585
- '."cdate" REGEXP @' +
1586
- cdate +
1587
- ')';
1588
- params[cdate] = curValue[1];
1589
- break;
1590
- } else if (curValue[0] === 'mdate') {
1721
+ if (
1722
+ curValue[0] === 'cdate' ||
1723
+ curValue[0] === 'mdate' ||
1724
+ (this.nymph.tilmeld != null &&
1725
+ (curValue[0] === 'user' ||
1726
+ curValue[0] === 'group' ||
1727
+ curValue[0] === 'acUser' ||
1728
+ curValue[0] === 'acGroup' ||
1729
+ curValue[0] === 'acOther' ||
1730
+ curValue[0] === 'acRead' ||
1731
+ curValue[0] === 'acWrite' ||
1732
+ curValue[0] === 'acFull'))
1733
+ ) {
1591
1734
  if (curQuery) {
1592
1735
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1593
1736
  }
1594
- const mdate = `param${++count.i}`;
1737
+ const value = `param${++count.i}`;
1595
1738
  curQuery +=
1596
1739
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1597
- '(' +
1740
+ '(lower(' +
1598
1741
  ieTable +
1599
- '."mdate" REGEXP @' +
1600
- mdate +
1601
- ')';
1602
- params[mdate] = curValue[1];
1603
- break;
1742
+ '.' +
1743
+ SQLite3Driver.escape(curValue[0]) +
1744
+ ') REGEXP lower(@' +
1745
+ value +
1746
+ '))';
1747
+ params[value] = curValue[1];
1604
1748
  } else {
1605
1749
  if (curQuery) {
1606
1750
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1624,34 +1768,33 @@ export default class SQLite3Driver extends NymphDriver {
1624
1768
  break;
1625
1769
  case 'like':
1626
1770
  case '!like':
1627
- if (curValue[0] === 'cdate') {
1628
- if (curQuery) {
1629
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1630
- }
1631
- const cdate = `param${++count.i}`;
1632
- curQuery +=
1633
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1634
- '(' +
1635
- ieTable +
1636
- '."cdate" LIKE @' +
1637
- cdate +
1638
- " ESCAPE '\\')";
1639
- params[cdate] = curValue[1];
1640
- break;
1641
- } else if (curValue[0] === 'mdate') {
1771
+ if (
1772
+ curValue[0] === 'cdate' ||
1773
+ curValue[0] === 'mdate' ||
1774
+ (this.nymph.tilmeld != null &&
1775
+ (curValue[0] === 'user' ||
1776
+ curValue[0] === 'group' ||
1777
+ curValue[0] === 'acUser' ||
1778
+ curValue[0] === 'acGroup' ||
1779
+ curValue[0] === 'acOther' ||
1780
+ curValue[0] === 'acRead' ||
1781
+ curValue[0] === 'acWrite' ||
1782
+ curValue[0] === 'acFull'))
1783
+ ) {
1642
1784
  if (curQuery) {
1643
1785
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1644
1786
  }
1645
- const mdate = `param${++count.i}`;
1787
+ const value = `param${++count.i}`;
1646
1788
  curQuery +=
1647
1789
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1648
1790
  '(' +
1649
1791
  ieTable +
1650
- '."mdate" LIKE @' +
1651
- mdate +
1792
+ '.' +
1793
+ SQLite3Driver.escape(curValue[0]) +
1794
+ ' LIKE @' +
1795
+ value +
1652
1796
  " ESCAPE '\\')";
1653
- params[mdate] = curValue[1];
1654
- break;
1797
+ params[value] = curValue[1];
1655
1798
  } else {
1656
1799
  if (curQuery) {
1657
1800
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1675,34 +1818,33 @@ export default class SQLite3Driver extends NymphDriver {
1675
1818
  break;
1676
1819
  case 'ilike':
1677
1820
  case '!ilike':
1678
- if (curValue[0] === 'cdate') {
1679
- if (curQuery) {
1680
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1681
- }
1682
- const cdate = `param${++count.i}`;
1683
- curQuery +=
1684
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1685
- '(' +
1686
- ieTable +
1687
- '."cdate" LIKE @' +
1688
- cdate +
1689
- " ESCAPE '\\')";
1690
- params[cdate] = curValue[1];
1691
- break;
1692
- } else if (curValue[0] === 'mdate') {
1821
+ if (
1822
+ curValue[0] === 'cdate' ||
1823
+ curValue[0] === 'mdate' ||
1824
+ (this.nymph.tilmeld != null &&
1825
+ (curValue[0] === 'user' ||
1826
+ curValue[0] === 'group' ||
1827
+ curValue[0] === 'acUser' ||
1828
+ curValue[0] === 'acGroup' ||
1829
+ curValue[0] === 'acOther' ||
1830
+ curValue[0] === 'acRead' ||
1831
+ curValue[0] === 'acWrite' ||
1832
+ curValue[0] === 'acFull'))
1833
+ ) {
1693
1834
  if (curQuery) {
1694
1835
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1695
1836
  }
1696
- const mdate = `param${++count.i}`;
1837
+ const value = `param${++count.i}`;
1697
1838
  curQuery +=
1698
1839
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1699
- '(' +
1840
+ '(lower(' +
1700
1841
  ieTable +
1701
- '."mdate" LIKE @' +
1702
- mdate +
1703
- " ESCAPE '\\')";
1704
- params[mdate] = curValue[1];
1705
- break;
1842
+ '.' +
1843
+ SQLite3Driver.escape(curValue[0]) +
1844
+ ') LIKE lower(@' +
1845
+ value +
1846
+ ") ESCAPE '\\')";
1847
+ params[value] = curValue[1];
1706
1848
  } else {
1707
1849
  if (curQuery) {
1708
1850
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1726,30 +1868,31 @@ export default class SQLite3Driver extends NymphDriver {
1726
1868
  break;
1727
1869
  case 'gt':
1728
1870
  case '!gt':
1729
- if (curValue[0] === 'cdate') {
1730
- if (curQuery) {
1731
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1732
- }
1733
- const cdate = `param${++count.i}`;
1734
- curQuery +=
1735
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1736
- ieTable +
1737
- '."cdate">@' +
1738
- cdate;
1739
- params[cdate] = Number(curValue[1]);
1740
- break;
1741
- } else if (curValue[0] === 'mdate') {
1871
+ if (
1872
+ curValue[0] === 'cdate' ||
1873
+ curValue[0] === 'mdate' ||
1874
+ (this.nymph.tilmeld != null &&
1875
+ (curValue[0] === 'user' ||
1876
+ curValue[0] === 'group' ||
1877
+ curValue[0] === 'acUser' ||
1878
+ curValue[0] === 'acGroup' ||
1879
+ curValue[0] === 'acOther' ||
1880
+ curValue[0] === 'acRead' ||
1881
+ curValue[0] === 'acWrite' ||
1882
+ curValue[0] === 'acFull'))
1883
+ ) {
1742
1884
  if (curQuery) {
1743
1885
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1744
1886
  }
1745
- const mdate = `param${++count.i}`;
1887
+ const value = `param${++count.i}`;
1746
1888
  curQuery +=
1747
1889
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1748
1890
  ieTable +
1749
- '."mdate">@' +
1750
- mdate;
1751
- params[mdate] = Number(curValue[1]);
1752
- break;
1891
+ '.' +
1892
+ SQLite3Driver.escape(curValue[0]) +
1893
+ '>@' +
1894
+ value;
1895
+ params[value] = Number(curValue[1]);
1753
1896
  } else {
1754
1897
  if (curQuery) {
1755
1898
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1773,30 +1916,31 @@ export default class SQLite3Driver extends NymphDriver {
1773
1916
  break;
1774
1917
  case 'gte':
1775
1918
  case '!gte':
1776
- if (curValue[0] === 'cdate') {
1777
- if (curQuery) {
1778
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1779
- }
1780
- const cdate = `param${++count.i}`;
1781
- curQuery +=
1782
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1783
- ieTable +
1784
- '."cdate">=@' +
1785
- cdate;
1786
- params[cdate] = Number(curValue[1]);
1787
- break;
1788
- } else if (curValue[0] === 'mdate') {
1919
+ if (
1920
+ curValue[0] === 'cdate' ||
1921
+ curValue[0] === 'mdate' ||
1922
+ (this.nymph.tilmeld != null &&
1923
+ (curValue[0] === 'user' ||
1924
+ curValue[0] === 'group' ||
1925
+ curValue[0] === 'acUser' ||
1926
+ curValue[0] === 'acGroup' ||
1927
+ curValue[0] === 'acOther' ||
1928
+ curValue[0] === 'acRead' ||
1929
+ curValue[0] === 'acWrite' ||
1930
+ curValue[0] === 'acFull'))
1931
+ ) {
1789
1932
  if (curQuery) {
1790
1933
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1791
1934
  }
1792
- const mdate = `param${++count.i}`;
1935
+ const value = `param${++count.i}`;
1793
1936
  curQuery +=
1794
1937
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1795
1938
  ieTable +
1796
- '."mdate">=@' +
1797
- mdate;
1798
- params[mdate] = Number(curValue[1]);
1799
- break;
1939
+ '.' +
1940
+ SQLite3Driver.escape(curValue[0]) +
1941
+ '>=@' +
1942
+ value;
1943
+ params[value] = Number(curValue[1]);
1800
1944
  } else {
1801
1945
  if (curQuery) {
1802
1946
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1820,30 +1964,31 @@ export default class SQLite3Driver extends NymphDriver {
1820
1964
  break;
1821
1965
  case 'lt':
1822
1966
  case '!lt':
1823
- if (curValue[0] === 'cdate') {
1824
- if (curQuery) {
1825
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1826
- }
1827
- const cdate = `param${++count.i}`;
1828
- curQuery +=
1829
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1830
- ieTable +
1831
- '."cdate"<@' +
1832
- cdate;
1833
- params[cdate] = Number(curValue[1]);
1834
- break;
1835
- } else if (curValue[0] === 'mdate') {
1967
+ if (
1968
+ curValue[0] === 'cdate' ||
1969
+ curValue[0] === 'mdate' ||
1970
+ (this.nymph.tilmeld != null &&
1971
+ (curValue[0] === 'user' ||
1972
+ curValue[0] === 'group' ||
1973
+ curValue[0] === 'acUser' ||
1974
+ curValue[0] === 'acGroup' ||
1975
+ curValue[0] === 'acOther' ||
1976
+ curValue[0] === 'acRead' ||
1977
+ curValue[0] === 'acWrite' ||
1978
+ curValue[0] === 'acFull'))
1979
+ ) {
1836
1980
  if (curQuery) {
1837
1981
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1838
1982
  }
1839
- const mdate = `param${++count.i}`;
1983
+ const value = `param${++count.i}`;
1840
1984
  curQuery +=
1841
1985
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1842
1986
  ieTable +
1843
- '."mdate"<@' +
1844
- mdate;
1845
- params[mdate] = Number(curValue[1]);
1846
- break;
1987
+ '.' +
1988
+ SQLite3Driver.escape(curValue[0]) +
1989
+ '<@' +
1990
+ value;
1991
+ params[value] = Number(curValue[1]);
1847
1992
  } else {
1848
1993
  if (curQuery) {
1849
1994
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1867,30 +2012,31 @@ export default class SQLite3Driver extends NymphDriver {
1867
2012
  break;
1868
2013
  case 'lte':
1869
2014
  case '!lte':
1870
- if (curValue[0] === 'cdate') {
1871
- if (curQuery) {
1872
- curQuery += typeIsOr ? ' OR ' : ' AND ';
1873
- }
1874
- const cdate = `param${++count.i}`;
1875
- curQuery +=
1876
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1877
- ieTable +
1878
- '."cdate"<=@' +
1879
- cdate;
1880
- params[cdate] = Number(curValue[1]);
1881
- break;
1882
- } else if (curValue[0] === 'mdate') {
2015
+ if (
2016
+ curValue[0] === 'cdate' ||
2017
+ curValue[0] === 'mdate' ||
2018
+ (this.nymph.tilmeld != null &&
2019
+ (curValue[0] === 'user' ||
2020
+ curValue[0] === 'group' ||
2021
+ curValue[0] === 'acUser' ||
2022
+ curValue[0] === 'acGroup' ||
2023
+ curValue[0] === 'acOther' ||
2024
+ curValue[0] === 'acRead' ||
2025
+ curValue[0] === 'acWrite' ||
2026
+ curValue[0] === 'acFull'))
2027
+ ) {
1883
2028
  if (curQuery) {
1884
2029
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1885
2030
  }
1886
- const mdate = `param${++count.i}`;
2031
+ const value = `param${++count.i}`;
1887
2032
  curQuery +=
1888
2033
  (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1889
2034
  ieTable +
1890
- '."mdate"<=@' +
1891
- mdate;
1892
- params[mdate] = Number(curValue[1]);
1893
- break;
2035
+ '.' +
2036
+ SQLite3Driver.escape(curValue[0]) +
2037
+ '<=@' +
2038
+ value;
2039
+ params[value] = Number(curValue[1]);
1894
2040
  } else {
1895
2041
  if (curQuery) {
1896
2042
  curQuery += typeIsOr ? ' OR ' : ' AND ';
@@ -1925,21 +2071,58 @@ export default class SQLite3Driver extends NymphDriver {
1925
2071
  if (curQuery) {
1926
2072
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1927
2073
  }
1928
- const name = `param${++count.i}`;
1929
- const guid = `param${++count.i}`;
1930
- curQuery +=
1931
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1932
- 'EXISTS (SELECT "guid" FROM ' +
1933
- SQLite3Driver.escape(this.prefix + 'references_' + etype) +
1934
- ' WHERE "guid"=' +
1935
- ieTable +
1936
- '."guid" AND "name"=@' +
1937
- name +
1938
- ' AND "reference"=@' +
1939
- guid +
1940
- ')';
1941
- params[name] = curValue[0];
1942
- params[guid] = curQguid;
2074
+ if (
2075
+ this.nymph.tilmeld != null &&
2076
+ (curValue[0] === 'user' || curValue[0] === 'group')
2077
+ ) {
2078
+ const guid = `param${++count.i}`;
2079
+ curQuery +=
2080
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2081
+ ieTable +
2082
+ '.' +
2083
+ SQLite3Driver.escape(curValue[0]) +
2084
+ '=@' +
2085
+ guid;
2086
+ params[guid] = curQguid;
2087
+ } else if (
2088
+ this.nymph.tilmeld != null &&
2089
+ (curValue[0] === 'acRead' ||
2090
+ curValue[0] === 'acWrite' ||
2091
+ curValue[0] === 'acFull')
2092
+ ) {
2093
+ const guid = `param${++count.i}`;
2094
+ curQuery +=
2095
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2096
+ ieTable +
2097
+ '.' +
2098
+ SQLite3Driver.escape(curValue[0]) +
2099
+ ' LIKE @' +
2100
+ guid +
2101
+ " ESCAPE '\\'";
2102
+ params[guid] =
2103
+ '%,' +
2104
+ curQguid
2105
+ .replace('\\', '\\\\')
2106
+ .replace('%', '\\%')
2107
+ .replace('_', '\\_') +
2108
+ ',%';
2109
+ } else {
2110
+ const name = `param${++count.i}`;
2111
+ const guid = `param${++count.i}`;
2112
+ curQuery +=
2113
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2114
+ 'EXISTS (SELECT "guid" FROM ' +
2115
+ SQLite3Driver.escape(this.prefix + 'references_' + etype) +
2116
+ ' WHERE "guid"=' +
2117
+ ieTable +
2118
+ '."guid" AND "name"=@' +
2119
+ name +
2120
+ ' AND "reference"=@' +
2121
+ guid +
2122
+ ')';
2123
+ params[name] = curValue[0];
2124
+ params[guid] = curQguid;
2125
+ }
1943
2126
  break;
1944
2127
  case 'selector':
1945
2128
  case '!selector':
@@ -1971,44 +2154,135 @@ export default class SQLite3Driver extends NymphDriver {
1971
2154
  ];
1972
2155
  const QrefEntityClass = qrefOptions.class as EntityConstructor;
1973
2156
  etypes.push(QrefEntityClass.ETYPE);
1974
- const qrefQuery = this.makeEntityQuery(
1975
- {
1976
- ...qrefOptions,
1977
- sort: qrefOptions.sort ?? null,
1978
- return: 'guid',
1979
- class: QrefEntityClass,
1980
- },
1981
- qrefSelectors,
1982
- QrefEntityClass.ETYPE,
1983
- count,
1984
- params,
1985
- false,
1986
- makeTableSuffix(),
1987
- etypes,
1988
- 'r' + referenceTableSuffix + '."reference"',
1989
- );
1990
2157
  if (curQuery) {
1991
2158
  curQuery += typeIsOr ? ' OR ' : ' AND ';
1992
2159
  }
1993
- const qrefName = `param${++count.i}`;
1994
- curQuery +=
1995
- (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
1996
- 'EXISTS (SELECT "guid" FROM ' +
1997
- SQLite3Driver.escape(this.prefix + 'references_' + etype) +
1998
- ' r' +
1999
- referenceTableSuffix +
2000
- ' WHERE r' +
2001
- referenceTableSuffix +
2002
- '."guid"=' +
2003
- ieTable +
2004
- '."guid" AND r' +
2005
- referenceTableSuffix +
2006
- '."name"=@' +
2007
- qrefName +
2008
- ' AND EXISTS (' +
2009
- qrefQuery.query +
2010
- '))';
2011
- params[qrefName] = curValue[0];
2160
+ if (
2161
+ this.nymph.tilmeld != null &&
2162
+ (curValue[0] === 'user' || curValue[0] === 'group')
2163
+ ) {
2164
+ const qrefQuery = this.makeEntityQuery(
2165
+ {
2166
+ ...qrefOptions,
2167
+ sort: qrefOptions.sort ?? null,
2168
+ return: 'guid',
2169
+ class: QrefEntityClass,
2170
+ },
2171
+ qrefSelectors,
2172
+ QrefEntityClass.ETYPE,
2173
+ count,
2174
+ params,
2175
+ false,
2176
+ makeTableSuffix(),
2177
+ etypes,
2178
+ 'r' +
2179
+ referenceTableSuffix +
2180
+ '.' +
2181
+ SQLite3Driver.escape(curValue[0]),
2182
+ );
2183
+
2184
+ curQuery +=
2185
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2186
+ 'EXISTS (SELECT "guid" FROM ' +
2187
+ SQLite3Driver.escape(this.prefix + 'entities_' + etype) +
2188
+ ' r' +
2189
+ referenceTableSuffix +
2190
+ ' WHERE r' +
2191
+ referenceTableSuffix +
2192
+ '."guid"=' +
2193
+ ieTable +
2194
+ '."guid" AND EXISTS (' +
2195
+ qrefQuery.query +
2196
+ '))';
2197
+ } else if (
2198
+ this.nymph.tilmeld != null &&
2199
+ (curValue[0] === 'acRead' ||
2200
+ curValue[0] === 'acWrite' ||
2201
+ curValue[0] === 'acFull')
2202
+ ) {
2203
+ const qrefQuery = this.makeEntityQuery(
2204
+ {
2205
+ ...qrefOptions,
2206
+ sort: qrefOptions.sort ?? null,
2207
+ return: 'guid',
2208
+ class: QrefEntityClass,
2209
+ },
2210
+ qrefSelectors,
2211
+ QrefEntityClass.ETYPE,
2212
+ count,
2213
+ params,
2214
+ false,
2215
+ makeTableSuffix(),
2216
+ etypes,
2217
+ 'r' + referenceTableSuffix + '."ref"',
2218
+ );
2219
+
2220
+ const splitTableSuffix = makeTableSuffix();
2221
+ curQuery +=
2222
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2223
+ `EXISTS (SELECT "guid", "ref" FROM (
2224
+ WITH RECURSIVE "spl${splitTableSuffix}" AS (
2225
+ SELECT
2226
+ "guid",
2227
+ SUBSTR(${SQLite3Driver.escape(curValue[0])}, 1, INSTR(${SQLite3Driver.escape(curValue[0])}, ',') - 1) AS "ref",
2228
+ SUBSTR(${SQLite3Driver.escape(curValue[0])}, INSTR(${SQLite3Driver.escape(curValue[0])}, ',') + 1) AS "remainder"
2229
+ FROM ${SQLite3Driver.escape(this.prefix + 'entities_' + etype)}
2230
+ UNION ALL
2231
+ SELECT
2232
+ "guid",
2233
+ SUBSTR("remainder", 1, INSTR("remainder", ',') - 1) AS "ref",
2234
+ SUBSTR("remainder", INSTR("remainder", ',') + 1) AS "remainder"
2235
+ FROM "spl${splitTableSuffix}" WHERE "remainder" != ''
2236
+ )
2237
+ SELECT "guid", "ref" FROM "spl${splitTableSuffix}" WHERE "ref" != ''
2238
+ ) ` +
2239
+ ' r' +
2240
+ referenceTableSuffix +
2241
+ ' WHERE r' +
2242
+ referenceTableSuffix +
2243
+ '."guid"=' +
2244
+ ieTable +
2245
+ '."guid" AND EXISTS (' +
2246
+ qrefQuery.query +
2247
+ '))';
2248
+ } else {
2249
+ const qrefQuery = this.makeEntityQuery(
2250
+ {
2251
+ ...qrefOptions,
2252
+ sort: qrefOptions.sort ?? null,
2253
+ return: 'guid',
2254
+ class: QrefEntityClass,
2255
+ },
2256
+ qrefSelectors,
2257
+ QrefEntityClass.ETYPE,
2258
+ count,
2259
+ params,
2260
+ false,
2261
+ makeTableSuffix(),
2262
+ etypes,
2263
+ 'r' + referenceTableSuffix + '."reference"',
2264
+ );
2265
+
2266
+ const qrefName = `param${++count.i}`;
2267
+ curQuery +=
2268
+ (xor(typeIsNot, clauseNot) ? 'NOT ' : '') +
2269
+ 'EXISTS (SELECT "guid" FROM ' +
2270
+ SQLite3Driver.escape(this.prefix + 'references_' + etype) +
2271
+ ' r' +
2272
+ referenceTableSuffix +
2273
+ ' WHERE r' +
2274
+ referenceTableSuffix +
2275
+ '."guid"=' +
2276
+ ieTable +
2277
+ '."guid" AND r' +
2278
+ referenceTableSuffix +
2279
+ '."name"=@' +
2280
+ qrefName +
2281
+ ' AND EXISTS (' +
2282
+ qrefQuery.query +
2283
+ '))';
2284
+ params[qrefName] = curValue[0];
2285
+ }
2012
2286
  break;
2013
2287
  }
2014
2288
  }
@@ -2095,6 +2369,14 @@ export default class SQLite3Driver extends NymphDriver {
2095
2369
  ${eTable}."tags",
2096
2370
  ${eTable}."cdate",
2097
2371
  ${eTable}."mdate",
2372
+ ${eTable}."user",
2373
+ ${eTable}."group",
2374
+ ${eTable}."acUser",
2375
+ ${eTable}."acGroup",
2376
+ ${eTable}."acOther",
2377
+ ${eTable}."acRead",
2378
+ ${eTable}."acWrite",
2379
+ ${eTable}."acFull",
2098
2380
  ${dTable}."name",
2099
2381
  ${dTable}."value",
2100
2382
  json(${dTable}."json") as "json",
@@ -2163,6 +2445,14 @@ export default class SQLite3Driver extends NymphDriver {
2163
2445
  ${eTable}."tags",
2164
2446
  ${eTable}."cdate",
2165
2447
  ${eTable}."mdate",
2448
+ ${eTable}."user",
2449
+ ${eTable}."group",
2450
+ ${eTable}."acUser",
2451
+ ${eTable}."acGroup",
2452
+ ${eTable}."acOther",
2453
+ ${eTable}."acRead",
2454
+ ${eTable}."acWrite",
2455
+ ${eTable}."acFull",
2166
2456
  ${dTable}."name",
2167
2457
  ${dTable}."value",
2168
2458
  json(${dTable}."json") as "json",
@@ -2191,6 +2481,14 @@ export default class SQLite3Driver extends NymphDriver {
2191
2481
  ${eTable}."tags",
2192
2482
  ${eTable}."cdate",
2193
2483
  ${eTable}."mdate",
2484
+ ${eTable}."user",
2485
+ ${eTable}."group",
2486
+ ${eTable}."acUser",
2487
+ ${eTable}."acGroup",
2488
+ ${eTable}."acOther",
2489
+ ${eTable}."acRead",
2490
+ ${eTable}."acWrite",
2491
+ ${eTable}."acFull",
2194
2492
  ${dTable}."name",
2195
2493
  ${dTable}."value",
2196
2494
  json(${dTable}."json") as "json",
@@ -2285,6 +2583,14 @@ export default class SQLite3Driver extends NymphDriver {
2285
2583
  : [],
2286
2584
  cdate: Number(row.cdate),
2287
2585
  mdate: Number(row.mdate),
2586
+ user: row.user,
2587
+ group: row.group,
2588
+ acUser: row.acUser,
2589
+ acGroup: row.acGroup,
2590
+ acOther: row.acOther,
2591
+ acRead: row.acRead?.slice(1, -1).split(',') ?? [],
2592
+ acWrite: row.acWrite?.slice(1, -1).split(',') ?? [],
2593
+ acFull: row.acFull?.slice(1, -1).split(',') ?? [],
2288
2594
  }),
2289
2595
  (row) => ({
2290
2596
  name: row.name,
@@ -3106,6 +3412,66 @@ export default class SQLite3Driver extends NymphDriver {
3106
3412
  return this.nymph;
3107
3413
  }
3108
3414
 
3415
+ private async removeTilmeldOldRows(etype: string) {
3416
+ await this.startTransaction('nymph-remove-tilmeld-rows');
3417
+ try {
3418
+ for (let name of [
3419
+ 'user',
3420
+ 'group',
3421
+ 'acUser',
3422
+ 'acGroup',
3423
+ 'acOther',
3424
+ 'acRead',
3425
+ 'acWrite',
3426
+ 'acFull',
3427
+ ]) {
3428
+ this.queryRun(
3429
+ `DELETE FROM ${SQLite3Driver.escape(
3430
+ `${this.prefix}data_${etype}`,
3431
+ )} WHERE "name"=@name;`,
3432
+ {
3433
+ etypes: [etype],
3434
+ params: {
3435
+ name,
3436
+ },
3437
+ },
3438
+ );
3439
+ this.queryRun(
3440
+ `DELETE FROM ${SQLite3Driver.escape(
3441
+ `${this.prefix}references_${etype}`,
3442
+ )} WHERE "name"=@name;`,
3443
+ {
3444
+ etypes: [etype],
3445
+ params: {
3446
+ name,
3447
+ },
3448
+ },
3449
+ );
3450
+ this.queryRun(
3451
+ `DELETE FROM ${SQLite3Driver.escape(
3452
+ `${this.prefix}tokens_${etype}`,
3453
+ )} WHERE "name"=@name;`,
3454
+ {
3455
+ etypes: [etype],
3456
+ params: {
3457
+ name,
3458
+ },
3459
+ },
3460
+ );
3461
+ }
3462
+ } catch (e: any) {
3463
+ this.nymph.config.debugError(
3464
+ 'sqlite3',
3465
+ `Remove tilmeld rows error: "${e}"`,
3466
+ );
3467
+ await this.rollback('nymph-remove-tilmeld-rows');
3468
+ throw e;
3469
+ }
3470
+
3471
+ await this.commit('nymph-remove-tilmeld-rows');
3472
+ return true;
3473
+ }
3474
+
3109
3475
  public async needsMigration(): Promise<
3110
3476
  'json' | 'tokens' | 'tilmeldColumns' | false
3111
3477
  > {
@@ -3165,7 +3531,9 @@ export default class SQLite3Driver extends NymphDriver {
3165
3531
  return false;
3166
3532
  }
3167
3533
 
3168
- public async liveMigration(migrationType: 'tokenTables' | 'tilmeldColumns') {
3534
+ public async liveMigration(
3535
+ migrationType: 'tokenTables' | 'tilmeldColumns' | 'tilmeldRemoveOldRows',
3536
+ ) {
3169
3537
  if (migrationType === 'tokenTables') {
3170
3538
  const etypes = await this.getEtypes();
3171
3539
 
@@ -3178,6 +3546,12 @@ export default class SQLite3Driver extends NymphDriver {
3178
3546
  for (let etype of etypes) {
3179
3547
  this.addTilmeldColumnsAndIndexes(etype);
3180
3548
  }
3549
+ } else if (migrationType === 'tilmeldRemoveOldRows') {
3550
+ const etypes = await this.getEtypes();
3551
+
3552
+ for (let etype of etypes) {
3553
+ await this.removeTilmeldOldRows(etype);
3554
+ }
3181
3555
  }
3182
3556
  }
3183
3557
  }