pogi 2.11.0 → 3.0.0-beta3
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/.env +5 -0
- package/.vscode/launch.json +47 -15
- package/jest.config.js +23 -0
- package/lib/bin/generateInterface.js +3 -3
- package/lib/bin/generateInterface.js.map +1 -1
- package/lib/connectionOptions.d.ts +7 -0
- package/lib/index.d.ts +1 -1
- package/lib/pgConverters.d.ts +9 -10
- package/lib/pgConverters.js +44 -39
- package/lib/pgConverters.js.map +1 -1
- package/lib/pgConverters.test.d.ts +1 -0
- package/lib/pgConverters.test.js +13 -0
- package/lib/pgConverters.test.js.map +1 -0
- package/lib/pgDb.d.ts +23 -31
- package/lib/pgDb.js +122 -103
- package/lib/pgDb.js.map +1 -1
- package/lib/pgDb.test.d.ts +1 -0
- package/lib/pgDb.test.js +1126 -0
- package/lib/pgDb.test.js.map +1 -0
- package/lib/pgDbInterface.d.ts +22 -0
- package/lib/pgDbInterface.js +11 -0
- package/lib/pgDbInterface.js.map +1 -0
- package/lib/pgDbOperators.d.ts +3 -3
- package/lib/pgDbOperators.js +4 -7
- package/lib/pgDbOperators.js.map +1 -1
- package/lib/pgDbOperators.test.d.ts +1 -0
- package/lib/pgDbOperators.test.js +313 -0
- package/lib/pgDbOperators.test.js.map +1 -0
- package/lib/pgSchema.d.ts +2 -3
- package/lib/pgSchema.js.map +1 -1
- package/lib/pgSchemaInterface.d.ts +0 -0
- package/lib/pgSchemaInterface.js +2 -0
- package/lib/pgSchemaInterface.js.map +1 -0
- package/lib/pgTable.d.ts +13 -38
- package/lib/pgTable.js +54 -54
- package/lib/pgTable.js.map +1 -1
- package/lib/pgTableInterface.d.ts +28 -0
- package/lib/pgTableInterface.js +4 -0
- package/lib/pgTableInterface.js.map +1 -0
- package/lib/pgUtils.d.ts +16 -6
- package/lib/pgUtils.js +162 -31
- package/lib/pgUtils.js.map +1 -1
- package/lib/queryAble.d.ts +16 -53
- package/lib/queryAble.js +58 -50
- package/lib/queryAble.js.map +1 -1
- package/lib/queryAbleInterface.d.ts +55 -0
- package/lib/queryAbleInterface.js +7 -0
- package/lib/queryAbleInterface.js.map +1 -0
- package/lib/queryWhere.d.ts +2 -2
- package/lib/queryWhere.js +19 -23
- package/lib/queryWhere.js.map +1 -1
- package/lib/test/pgDbOperatorSpec.d.ts +1 -0
- package/lib/test/pgDbOperatorSpec.js +326 -0
- package/lib/test/pgDbOperatorSpec.js.map +1 -0
- package/lib/test/pgDbSpec.d.ts +1 -0
- package/lib/test/pgDbSpec.js +1139 -0
- package/lib/test/pgDbSpec.js.map +1 -0
- package/lib/test/pgServiceRestartTest.d.ts +1 -0
- package/lib/test/pgServiceRestartTest.js +1532 -0
- package/lib/test/pgServiceRestartTest.js.map +1 -0
- package/package.json +21 -14
- package/{src/tsconfig.json → tsconfig.json} +11 -11
- package/spec/resources/init.sql +0 -122
- package/spec/resources/throw_exception.sql +0 -5
- package/spec/resources/tricky.sql +0 -13
- package/spec/run.js +0 -5
- package/spec/support/jasmine.json +0 -9
- package/src/bin/generateInterface.ts +0 -54
- package/src/connectionOptions.ts +0 -42
- package/src/index.ts +0 -6
- package/src/pgConverters.ts +0 -55
- package/src/pgDb.ts +0 -820
- package/src/pgDbLogger.ts +0 -13
- package/src/pgDbOperators.ts +0 -62
- package/src/pgSchema.ts +0 -15
- package/src/pgTable.ts +0 -401
- package/src/pgUtils.ts +0 -176
- package/src/queryAble.ts +0 -393
- package/src/queryWhere.ts +0 -326
- package/src/test/pgDbOperatorSpec.ts +0 -492
- package/src/test/pgDbSpec.ts +0 -1339
- package/src/test/pgServiceRestartTest.ts +0 -1500
package/src/queryWhere.ts
DELETED
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import operationsMap from "./pgDbOperators";
|
|
2
|
-
import {FieldType} from "./pgDb";
|
|
3
|
-
|
|
4
|
-
const _ = require("lodash");
|
|
5
|
-
const util = require("util");
|
|
6
|
-
|
|
7
|
-
class FieldAndOperator {
|
|
8
|
-
field: string;
|
|
9
|
-
quotedField: string;
|
|
10
|
-
operator: string;
|
|
11
|
-
originalOp: string;
|
|
12
|
-
mutator?: Function;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** public */
|
|
16
|
-
function generateWhere(conditions, fieldTypes: { [index: string]: FieldType }, tableName: string, placeholderOffset = 0, skipUndefined): { where: string, params: Array<any> } {
|
|
17
|
-
let result = generate({
|
|
18
|
-
params: [],
|
|
19
|
-
predicates: [],
|
|
20
|
-
offset: placeholderOffset,
|
|
21
|
-
}, conditions, fieldTypes, tableName, skipUndefined);
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
where: result.predicates.length > 0 ? ' WHERE ' + result.predicates.join(' AND ') : '',
|
|
25
|
-
params: result.params
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** private */
|
|
30
|
-
function generate(result, conditions, fieldTypes: { [index: string]: FieldType }, tableName: string, skipUndefined) {
|
|
31
|
-
_.each(conditions, (value, key) => {
|
|
32
|
-
//get the column field and the operator if specified
|
|
33
|
-
let fieldAndOperator = parseKey(key);
|
|
34
|
-
|
|
35
|
-
if (value === undefined) { //null is ok, but undefined is skipped if requested
|
|
36
|
-
if (skipUndefined === true) return;
|
|
37
|
-
throw new Error('Invalid conditions! Field value undefined: "' + fieldAndOperator.field + '". Either delete the field, set it to null or use the options.skipUndefined parameter.');
|
|
38
|
-
}
|
|
39
|
-
else if (fieldAndOperator.field === 'or' || fieldAndOperator.field === 'and') {
|
|
40
|
-
result = handleOrAnd(result, fieldAndOperator, value, fieldTypes, tableName, skipUndefined);
|
|
41
|
-
}
|
|
42
|
-
else if (value === null) {
|
|
43
|
-
result = handleNullValue(result, fieldAndOperator, value);
|
|
44
|
-
}
|
|
45
|
-
else if (Array.isArray(value)) {
|
|
46
|
-
result = handleArrayValue(result, fieldAndOperator, value, fieldTypes);
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
result = handleSingleValue(result, fieldAndOperator, value, fieldTypes, tableName);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function handleOrAnd(result, fieldAndOperator, value, fieldTypes: { [index: string]: FieldType }, tableName: string, skipUndefined) {
|
|
57
|
-
if (!Array.isArray(value)) {
|
|
58
|
-
value = [value];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let groupResult = _.reduce(value, (acc, v) => {
|
|
62
|
-
// assemble predicates for each subgroup in this 'or' array
|
|
63
|
-
let subResult = generate({
|
|
64
|
-
params: [],
|
|
65
|
-
predicates: [],
|
|
66
|
-
offset: result.params.length + acc.offset // ensure the offset from predicates outside the subgroup is counted
|
|
67
|
-
}, v, fieldTypes, tableName, skipUndefined);
|
|
68
|
-
|
|
69
|
-
// encapsulate and join the individual predicates with AND to create the complete subgroup predicate
|
|
70
|
-
acc.predicates.push(util.format('(%s)', subResult.predicates.join(' AND ')));
|
|
71
|
-
acc.params = acc.params.concat(subResult.params);
|
|
72
|
-
acc.offset += subResult.params.length;
|
|
73
|
-
|
|
74
|
-
return acc;
|
|
75
|
-
}, {
|
|
76
|
-
params: [],
|
|
77
|
-
predicates: [],
|
|
78
|
-
offset: result.offset
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// join the compiled subgroup predicates with OR, encapsulate, and push the
|
|
82
|
-
// complex predicate ("((x = $1 AND y = $2) OR (z = $3))") onto the result object
|
|
83
|
-
result.params = result.params.concat(groupResult.params);
|
|
84
|
-
if (groupResult.predicates.length) {
|
|
85
|
-
if (fieldAndOperator.field === 'and') {
|
|
86
|
-
result.predicates.push(util.format('(%s)', groupResult.predicates.join(' AND ')));
|
|
87
|
-
} else {
|
|
88
|
-
result.predicates.push(util.format('(%s)', groupResult.predicates.join(' OR ')));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return result;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function handleNullValue(result, fieldAndOperator, value) {
|
|
95
|
-
fieldAndOperator.operator = fieldAndOperator.operator === '=' ? 'IS' : 'IS NOT';
|
|
96
|
-
result.predicates.push(util.format('%s %s %s', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
|
|
97
|
-
return result;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function handleArrayValue(result, fieldAndOperator, value, fieldTypes: { [index: string]: FieldType }) {
|
|
101
|
-
if (fieldAndOperator.mutator) {
|
|
102
|
-
value = value.map(v => fieldAndOperator.mutator(v));
|
|
103
|
-
}
|
|
104
|
-
let fieldType = fieldTypes[fieldAndOperator.field];
|
|
105
|
-
|
|
106
|
-
if (fieldType == FieldType.JSON &&
|
|
107
|
-
['?|', '?&'].indexOf(fieldAndOperator.operator) != -1) {
|
|
108
|
-
result.params.push(value);
|
|
109
|
-
value = util.format("$%s", result.params.length + result.offset);
|
|
110
|
-
result.predicates.push(util.format('%s %s %s', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
else if (fieldType == FieldType.JSON &&
|
|
114
|
-
['@>', '<@', '&&'].indexOf(fieldAndOperator.operator) != -1) {
|
|
115
|
-
result.params.push(JSON.stringify(value));
|
|
116
|
-
value = util.format("$%s", result.params.length + result.offset);
|
|
117
|
-
result.predicates.push(util.format('%s %s %s', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
|
|
118
|
-
return result;
|
|
119
|
-
}
|
|
120
|
-
else if ((!fieldType || fieldType == FieldType.TIME) &&
|
|
121
|
-
['=', '<>', 'IN', 'NOT IN'].includes(fieldAndOperator.operator)) {
|
|
122
|
-
if (fieldAndOperator.operator === '=' || fieldAndOperator.operator === 'IN') {
|
|
123
|
-
fieldAndOperator.operator = '= ANY';
|
|
124
|
-
} else {
|
|
125
|
-
fieldAndOperator.operator = '<> ALL';
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (value.length === 0) { // avoid empty "[NOT] IN ()"
|
|
129
|
-
throw new Error('Invalid conditions! empty array for field:"' + fieldAndOperator.field + '" and operator:"' + fieldAndOperator.operator + '"');
|
|
130
|
-
//return result;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
result.params.push(value
|
|
134
|
-
.map(v => (fieldType == FieldType.TIME && !(value instanceof Date)) ? new Date(v) : v)
|
|
135
|
-
);
|
|
136
|
-
value = util.format("$%s", result.params.length + result.offset);
|
|
137
|
-
result.predicates.push(util.format('%s %s (%s)', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
|
|
138
|
-
return result;
|
|
139
|
-
}
|
|
140
|
-
else if (!fieldType && ['LIKE', 'ILIKE', 'SIMILAR TO', '~', '~*'].indexOf(fieldAndOperator.operator) != -1) {
|
|
141
|
-
//defaults to any
|
|
142
|
-
result.params.push(value);
|
|
143
|
-
value = util.format("$%s", result.params.length + result.offset);
|
|
144
|
-
result.predicates.push(util.format('%s %s ANY(%s)', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
147
|
-
else if (!fieldType && ['NOT LIKE', 'NOT ILIKE', 'NOT SIMILAR TO', '!~', '!~*'].indexOf(fieldAndOperator.operator) != -1) {
|
|
148
|
-
//defaults to all
|
|
149
|
-
result.params.push(value);
|
|
150
|
-
value = util.format("$%s", result.params.length + result.offset);
|
|
151
|
-
result.predicates.push(util.format('%s %s ALL(%s)', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
|
|
152
|
-
return result;
|
|
153
|
-
}
|
|
154
|
-
else if (fieldType == FieldType.ARRAY &&
|
|
155
|
-
['=', '<>', '<', '>', '<=', '>=', '@>', '<@', '&&'].indexOf(fieldAndOperator.operator) != -1) {
|
|
156
|
-
result.params.push(value);
|
|
157
|
-
value = util.format("$%s", result.params.length + result.offset);
|
|
158
|
-
result.predicates.push(util.format('%s %s %s', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
throw new Error('[325] Not implemented operator: "' + fieldAndOperator.operator + '" for field ' + fieldAndOperator.field + ' with type ' + fieldType);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
function handleSingleValue(result, fieldAndOperator: FieldAndOperator, value, fieldTypes: { [index: string]: FieldType }, tableName) {
|
|
167
|
-
if (fieldAndOperator.mutator) {
|
|
168
|
-
value = fieldAndOperator.mutator(value);
|
|
169
|
-
}
|
|
170
|
-
let fieldType = fieldTypes[fieldAndOperator.field];
|
|
171
|
-
if (fieldAndOperator.operator === '@@') {
|
|
172
|
-
/**
|
|
173
|
-
* field can be string -> to_tsquery(string)
|
|
174
|
-
* or object {lang:'english', txt:string} -> to_tsquery(o.lang, o.txt)
|
|
175
|
-
*/
|
|
176
|
-
if (typeof value == 'object') {
|
|
177
|
-
if (!(value.lang || value.language) || !(value.query || value.plainquery)) {
|
|
178
|
-
throw new Error('[499] only "lang"/"language" and "query/plainquery" properties are supported!');
|
|
179
|
-
}
|
|
180
|
-
if (fieldType == FieldType.TSVECTOR) {
|
|
181
|
-
//language is already set
|
|
182
|
-
result.params.push(value.lang || value.language);
|
|
183
|
-
result.params.push(value.query || value.plainquery);
|
|
184
|
-
let template = value.query ? "%s %s to_tsquery($%s, $%s)" : "%s %s plainto_tsquery($%s, $%s)";
|
|
185
|
-
result.predicates.push(util.format(template,
|
|
186
|
-
fieldAndOperator.quotedField,
|
|
187
|
-
fieldAndOperator.operator,
|
|
188
|
-
result.params.length - 1 + result.offset, //lang
|
|
189
|
-
result.params.length + result.offset //query
|
|
190
|
-
));
|
|
191
|
-
} else {
|
|
192
|
-
result.params.push(value.lang || value.language);
|
|
193
|
-
result.params.push(value.lang || value.language);
|
|
194
|
-
result.params.push(value.query || value.plainquery);
|
|
195
|
-
let template = value.query ? "to_tsvector($%s, %s) %s to_tsquery($%s, $%s)" : "to_tsvector($%s, %s) %s plainto_tsquery($%s, $%s)";
|
|
196
|
-
result.predicates.push(util.format(template,
|
|
197
|
-
result.params.length - 2 + result.offset, //lang
|
|
198
|
-
fieldAndOperator.quotedField,
|
|
199
|
-
fieldAndOperator.operator,
|
|
200
|
-
result.params.length - 1 + result.offset, //lang
|
|
201
|
-
result.params.length + result.offset //query
|
|
202
|
-
));
|
|
203
|
-
}
|
|
204
|
-
} else {
|
|
205
|
-
result.params.push(value);
|
|
206
|
-
let template = fieldType == FieldType.TSVECTOR ? "%s %s plainto_tsquery($%s)" : "to_tsvector(%s) %s plainto_tsquery($%s)";
|
|
207
|
-
result.predicates.push(util.format(template, fieldAndOperator.quotedField, fieldAndOperator.operator, result.params.length + result.offset));
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
else if (fieldType == FieldType.ARRAY) {
|
|
211
|
-
if (['=', '<>'].indexOf(fieldAndOperator.operator) != -1) {
|
|
212
|
-
if (fieldAndOperator.originalOp == '=*') {
|
|
213
|
-
result.params.push([value]);
|
|
214
|
-
value = util.format("$%s", result.params.length + result.offset);
|
|
215
|
-
result.predicates.push(util.format('%s && %s', fieldAndOperator.quotedField, value));
|
|
216
|
-
} else {
|
|
217
|
-
result.params.push(value);
|
|
218
|
-
value = util.format("$%s", result.params.length + result.offset);
|
|
219
|
-
result.predicates.push(util.format('%s %s ANY(%s)', value, fieldAndOperator.operator, fieldAndOperator.quotedField));
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
else if (['LIKE', 'ILIKE', 'NOT LIKE', 'NOT ILIKE', 'SIMILAR TO', 'NOT SIMILAR TO'].indexOf(fieldAndOperator.operator) != -1) {
|
|
223
|
-
result.params.push(value);
|
|
224
|
-
value = util.format("$%s", result.params.length + result.offset);
|
|
225
|
-
|
|
226
|
-
let q = 'EXISTS (SELECT * FROM (SELECT UNNEST(' + tableName + '.%s) _el) _arr WHERE _arr._el %s %s)';
|
|
227
|
-
result.predicates.push(util.format(q, fieldAndOperator.quotedField, fieldAndOperator.operator, value));
|
|
228
|
-
} else {
|
|
229
|
-
throw new Error('[326] Not implemented operator: "' + fieldAndOperator.operator + '" for type ' + fieldType);
|
|
230
|
-
}
|
|
231
|
-
} else {
|
|
232
|
-
result.params.push((fieldType == FieldType.TIME && !(value instanceof Date)) ? new Date(value) : value);
|
|
233
|
-
value = util.format("$%s", result.params.length + result.offset);
|
|
234
|
-
result.predicates.push(util.format('%s %s %s', fieldAndOperator.quotedField, fieldAndOperator.operator, value));
|
|
235
|
-
}
|
|
236
|
-
return result;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
function strip(arr) {
|
|
241
|
-
return arr.map((s) => s.trim()).filter(v => v != '');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function getOp(str) {
|
|
245
|
-
for (let i = 0; i < str.length; i++) {
|
|
246
|
-
if (operationsMap[str.substr(i)]) {
|
|
247
|
-
return str.substr(i);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return '';
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Parse out a criterion key into something more intelligible. Supports quoted
|
|
255
|
-
* field names and whitespace between components. If a function is applied on the field
|
|
256
|
-
* it takes the first quoted string to assume to be the column. (e.g. max("score") -> field: "score"
|
|
257
|
-
* in order to be able to recognize the field type)
|
|
258
|
-
*
|
|
259
|
-
* 'createdTs >' => {
|
|
260
|
-
* field: 'createdTs',
|
|
261
|
-
* quotedField: '"createdTs"',
|
|
262
|
-
* operator:'>',
|
|
263
|
-
* mutator:null
|
|
264
|
-
* }
|
|
265
|
-
*
|
|
266
|
-
* @param {String} key Key in a format resembling "field [JSON operation+path] operation"
|
|
267
|
-
* @return {Object} [description]
|
|
268
|
-
*/
|
|
269
|
-
function parseKey(key): FieldAndOperator {
|
|
270
|
-
key = key.trim();
|
|
271
|
-
|
|
272
|
-
let userOp = getOp(key);
|
|
273
|
-
if (userOp) {
|
|
274
|
-
key = key.substr(0, key.length - userOp.length)
|
|
275
|
-
}
|
|
276
|
-
let operation = operationsMap[userOp] || {};
|
|
277
|
-
let jsonRegexp = /(->[^>]|->>|#>[^>]|#>>)/;
|
|
278
|
-
|
|
279
|
-
let field;
|
|
280
|
-
let quotedField;
|
|
281
|
-
|
|
282
|
-
let quotedByUser = key.indexOf('"') > -1; //key[0]=='"'; -> lets make it possible to write transformed columns, e.g. LOWER("field")
|
|
283
|
-
if (quotedByUser) {
|
|
284
|
-
quotedField = key;
|
|
285
|
-
//field is used for find out the type of the field, so lets restore it if possible, grab the first quoted string
|
|
286
|
-
field = /[^"]*"([^"]*)".*/.exec(key)[1];
|
|
287
|
-
if (!quotedField || !field) {
|
|
288
|
-
console.error("Parsing error!");
|
|
289
|
-
}
|
|
290
|
-
} else {
|
|
291
|
-
let parts = strip(key.split(jsonRegexp));
|
|
292
|
-
|
|
293
|
-
field = parts.shift();
|
|
294
|
-
quotedField = util.format('"%s"', field);
|
|
295
|
-
|
|
296
|
-
if (parts.length > 1) {
|
|
297
|
-
let jsonOp = parts.shift();
|
|
298
|
-
let jsonKey = parts.shift();
|
|
299
|
-
|
|
300
|
-
// treat numeric json keys as array indices, otherwise quote it
|
|
301
|
-
if (isNaN(jsonKey) && jsonKey.indexOf("'") == -1) {
|
|
302
|
-
jsonKey = util.format("'%s'", jsonKey);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
quotedField = util.format('%s%s%s', quotedField, jsonOp, jsonKey);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (operation.fieldMutator) {
|
|
311
|
-
quotedField = operation.fieldMutator(field, quotedField);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
field: field,
|
|
317
|
-
quotedField: quotedField,
|
|
318
|
-
operator: (operation.operator || '=').toUpperCase(),
|
|
319
|
-
mutator: operation.mutator,
|
|
320
|
-
originalOp: userOp
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
export default generateWhere;
|
|
326
|
-
|