@terreno/api 0.11.1 → 0.11.2
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/dist/api.d.ts +3 -3
- package/dist/api.js +15 -15
- package/dist/populate.test.js +62 -0
- package/package.json +1 -1
- package/src/api.ts +18 -16
- package/src/populate.test.ts +70 -0
package/dist/api.d.ts
CHANGED
|
@@ -8,11 +8,11 @@ import { type TerrenoTransformer } from "./transformers";
|
|
|
8
8
|
export type JSONPrimitive = string | number | boolean | null;
|
|
9
9
|
export interface JSONArray extends Array<JSONValue> {
|
|
10
10
|
}
|
|
11
|
-
export
|
|
11
|
+
export interface JSONObject {
|
|
12
12
|
[member: string]: JSONValue;
|
|
13
|
-
}
|
|
13
|
+
}
|
|
14
14
|
export type JSONValue = JSONPrimitive | JSONObject | JSONArray;
|
|
15
|
-
export declare
|
|
15
|
+
export declare const addPopulateToQuery: (builtQuery: mongoose.Query<any[], any, Record<string, never>, any>, populatePaths?: PopulatePath[]) => mongoose.Query<any[], any, Record<string, never>, any, "find", Record<string, never>>;
|
|
16
16
|
/**
|
|
17
17
|
* @param a - the first number
|
|
18
18
|
* @param b - the second number
|
package/dist/api.js
CHANGED
|
@@ -119,8 +119,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
119
119
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
120
120
|
};
|
|
121
121
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
122
|
-
exports.gooseRestRouter = exports.asyncHandler = void 0;
|
|
123
|
-
exports.addPopulateToQuery = addPopulateToQuery;
|
|
122
|
+
exports.gooseRestRouter = exports.asyncHandler = exports.addPopulateToQuery = void 0;
|
|
124
123
|
exports.modelRouter = modelRouter;
|
|
125
124
|
/**
|
|
126
125
|
* This is the doc comment for api.ts
|
|
@@ -139,7 +138,7 @@ var openApiValidator_1 = require("./openApiValidator");
|
|
|
139
138
|
var permissions_1 = require("./permissions");
|
|
140
139
|
var transformers_1 = require("./transformers");
|
|
141
140
|
var utils_1 = require("./utils");
|
|
142
|
-
function
|
|
141
|
+
var addPopulateToQuery = function (builtQuery, populatePaths) {
|
|
143
142
|
var e_1, _a;
|
|
144
143
|
var paths = populatePaths !== null && populatePaths !== void 0 ? populatePaths : [];
|
|
145
144
|
var query = builtQuery;
|
|
@@ -159,7 +158,8 @@ function addPopulateToQuery(builtQuery, populatePaths) {
|
|
|
159
158
|
finally { if (e_1) throw e_1.error; }
|
|
160
159
|
}
|
|
161
160
|
return query;
|
|
162
|
-
}
|
|
161
|
+
};
|
|
162
|
+
exports.addPopulateToQuery = addPopulateToQuery;
|
|
163
163
|
// TODOS:
|
|
164
164
|
// Support bulk actions
|
|
165
165
|
// Support more complex query fields
|
|
@@ -169,7 +169,7 @@ var PAGINATION_QUERY_PARAMS = ["limit", "page", "sort"];
|
|
|
169
169
|
// Add support for more complex queries.
|
|
170
170
|
var COMPLEX_QUERY_PARAMS = ["$and", "$or"];
|
|
171
171
|
// Ensures query params are allowed. Also checks nested query params when using $and/$or.
|
|
172
|
-
function
|
|
172
|
+
var checkQueryParamAllowed = function (queryParam, queryParamValue, queryFields) {
|
|
173
173
|
var e_2, _a, e_3, _b;
|
|
174
174
|
if (queryFields === void 0) { queryFields = []; }
|
|
175
175
|
// Check the values of each of the complex query params. We don't support recursive queries here,
|
|
@@ -209,7 +209,7 @@ function checkQueryParamAllowed(queryParam, queryParamValue, queryFields) {
|
|
|
209
209
|
title: "".concat(queryParam, " is not allowed as a query param."),
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
|
-
}
|
|
212
|
+
};
|
|
213
213
|
// Handles dot notation patches, creates a normal object to be used for updates.
|
|
214
214
|
// function flattenDotNotationPatch(data: any) {
|
|
215
215
|
// const result = {};
|
|
@@ -231,7 +231,7 @@ function checkQueryParamAllowed(queryParam, queryParamValue, queryFields) {
|
|
|
231
231
|
// Helper to determine if validation should be enabled for a specific operation.
|
|
232
232
|
// When options.validation is not set, returns true — the middleware's own
|
|
233
233
|
// isConfigured check will decide whether to actually validate.
|
|
234
|
-
function
|
|
234
|
+
var shouldValidate = function (options, operation) {
|
|
235
235
|
var _a, _b, _c;
|
|
236
236
|
// Check route-specific validation option first
|
|
237
237
|
if (options.validation !== undefined) {
|
|
@@ -248,9 +248,9 @@ function shouldValidate(options, operation) {
|
|
|
248
248
|
}
|
|
249
249
|
// Default: let middleware's isConfigured check decide
|
|
250
250
|
return true;
|
|
251
|
-
}
|
|
251
|
+
};
|
|
252
252
|
// Get body validation middleware if validation is enabled
|
|
253
|
-
function
|
|
253
|
+
var getBodyValidationMiddleware = function (model, options, operation) {
|
|
254
254
|
var validationOptions = {};
|
|
255
255
|
if (!shouldValidate(options, operation)) {
|
|
256
256
|
validationOptions.enabled = false;
|
|
@@ -271,9 +271,9 @@ function getBodyValidationMiddleware(model, options, operation) {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
return (0, openApiValidator_1.validateModelRequestBody)(model, validationOptions);
|
|
274
|
-
}
|
|
274
|
+
};
|
|
275
275
|
// Get query validation middleware if validation is enabled
|
|
276
|
-
function
|
|
276
|
+
var getQueryValidationMiddleware = function (model, options) {
|
|
277
277
|
var querySchema = (0, openApiValidator_1.buildQuerySchemaFromFields)(model, options.queryFields);
|
|
278
278
|
var validationOptions = {};
|
|
279
279
|
if (!shouldValidate(options, "query")) {
|
|
@@ -283,7 +283,7 @@ function getQueryValidationMiddleware(model, options) {
|
|
|
283
283
|
validationOptions.onError = options.validation.onError;
|
|
284
284
|
}
|
|
285
285
|
return (0, openApiValidator_1.validateQueryParams)(querySchema, validationOptions);
|
|
286
|
-
}
|
|
286
|
+
};
|
|
287
287
|
function modelRouter(pathOrModel, modelOrOptions, maybeOptions) {
|
|
288
288
|
var model;
|
|
289
289
|
var options;
|
|
@@ -406,7 +406,7 @@ function _buildModelRouter(model, options) {
|
|
|
406
406
|
case 10:
|
|
407
407
|
_a.trys.push([10, 12, , 13]);
|
|
408
408
|
populateQuery = model.findById(data._id);
|
|
409
|
-
populateQuery = addPopulateToQuery(populateQuery, options.populatePaths);
|
|
409
|
+
populateQuery = (0, exports.addPopulateToQuery)(populateQuery, options.populatePaths);
|
|
410
410
|
return [4 /*yield*/, populateQuery.exec()];
|
|
411
411
|
case 11:
|
|
412
412
|
data = _a.sent();
|
|
@@ -568,7 +568,7 @@ function _buildModelRouter(model, options) {
|
|
|
568
568
|
else if (options.sort) {
|
|
569
569
|
builtQuery = builtQuery.sort(options.sort);
|
|
570
570
|
}
|
|
571
|
-
populatedQuery = addPopulateToQuery(builtQuery, options.populatePaths);
|
|
571
|
+
populatedQuery = (0, exports.addPopulateToQuery)(builtQuery, options.populatePaths);
|
|
572
572
|
_o.label = 7;
|
|
573
573
|
case 7:
|
|
574
574
|
_o.trys.push([7, 9, , 10]);
|
|
@@ -754,7 +754,7 @@ function _buildModelRouter(model, options) {
|
|
|
754
754
|
case 9:
|
|
755
755
|
if (!options.populatePaths) return [3 /*break*/, 11];
|
|
756
756
|
populateQuery = model.findById(doc._id);
|
|
757
|
-
populateQuery = addPopulateToQuery(populateQuery, options.populatePaths);
|
|
757
|
+
populateQuery = (0, exports.addPopulateToQuery)(populateQuery, options.populatePaths);
|
|
758
758
|
return [4 /*yield*/, populateQuery.exec()];
|
|
759
759
|
case 10:
|
|
760
760
|
doc = _b.sent();
|
package/dist/populate.test.js
CHANGED
|
@@ -281,4 +281,66 @@ var tests_1 = require("./tests");
|
|
|
281
281
|
});
|
|
282
282
|
(0, bun_test_1.expect)(result.properties).toBeDefined();
|
|
283
283
|
});
|
|
284
|
+
(0, bun_test_1.it)("uses openApiComponent $ref when provided", function () {
|
|
285
|
+
var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
|
|
286
|
+
populatePaths: [{ openApiComponent: "UserComponent", path: "ownerId" }],
|
|
287
|
+
});
|
|
288
|
+
(0, bun_test_1.expect)(result.properties.ownerId).toEqual({
|
|
289
|
+
$ref: "#/components/schemas/UserComponent",
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
(0, bun_test_1.it)("populates array ref fields (eatenBy)", function () {
|
|
293
|
+
var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
|
|
294
|
+
populatePaths: [{ path: "eatenBy" }],
|
|
295
|
+
});
|
|
296
|
+
(0, bun_test_1.expect)(result.properties.eatenBy).toBeDefined();
|
|
297
|
+
(0, bun_test_1.expect)(result.properties.eatenBy.items).toBeDefined();
|
|
298
|
+
});
|
|
299
|
+
(0, bun_test_1.it)("populates nested ref in sub-schema (likesIds.userId)", function () {
|
|
300
|
+
var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
|
|
301
|
+
populatePaths: [{ path: "likesIds.userId" }],
|
|
302
|
+
});
|
|
303
|
+
(0, bun_test_1.expect)(result.properties.likesIds).toBeDefined();
|
|
304
|
+
});
|
|
305
|
+
(0, bun_test_1.it)("includes virtuals from model schema", function () {
|
|
306
|
+
var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel);
|
|
307
|
+
(0, bun_test_1.expect)(result.properties.description).toBeDefined();
|
|
308
|
+
(0, bun_test_1.expect)(result.properties.description.type).toBe("any");
|
|
309
|
+
});
|
|
310
|
+
(0, bun_test_1.it)("includes virtuals from child schemas", function () {
|
|
311
|
+
var childSub = new mongoose_1.Schema({ amount: { description: "Amount", type: Number } });
|
|
312
|
+
childSub.virtual("displayAmount").get(function () {
|
|
313
|
+
return "".concat(this.amount, " units");
|
|
314
|
+
});
|
|
315
|
+
var parentSchema = new mongoose_1.Schema({
|
|
316
|
+
detail: { description: "Single embedded detail", type: childSub },
|
|
317
|
+
title: { description: "Title", type: String },
|
|
318
|
+
});
|
|
319
|
+
var ParentModel = mongoose_1.default.models.ParentWithChildVirtual ||
|
|
320
|
+
mongoose_1.default.model("ParentWithChildVirtual", parentSchema);
|
|
321
|
+
var result = (0, populate_1.getOpenApiSpecForModel)(ParentModel);
|
|
322
|
+
var detail = result.properties.detail;
|
|
323
|
+
(0, bun_test_1.expect)(detail.properties.displayAmount).toBeDefined();
|
|
324
|
+
(0, bun_test_1.expect)(detail.properties.displayAmount.type).toBe("any");
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
(0, bun_test_1.describe)("filterKeys (via getOpenApiSpecForModel populatePaths)", function () {
|
|
328
|
+
(0, bun_test_1.it)("filters populated fields using dot-notation keys", function () {
|
|
329
|
+
var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
|
|
330
|
+
populatePaths: [{ fields: ["name.nested"], path: "ownerId" }],
|
|
331
|
+
});
|
|
332
|
+
var ownerProps = result.properties.ownerId.properties;
|
|
333
|
+
(0, bun_test_1.expect)(ownerProps.name).toBeDefined();
|
|
334
|
+
(0, bun_test_1.expect)(typeof ownerProps.name).toBe("object");
|
|
335
|
+
});
|
|
336
|
+
(0, bun_test_1.it)("rejects prototype pollution keys in nested dot-notation", function () {
|
|
337
|
+
var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
|
|
338
|
+
populatePaths: [{ fields: ["__proto__.polluted"], path: "ownerId" }],
|
|
339
|
+
});
|
|
340
|
+
(0, bun_test_1.expect)(result.properties).toBeDefined();
|
|
341
|
+
(0, bun_test_1.expect)(Object.prototype.polluted).toBeUndefined();
|
|
342
|
+
var ownerProps = result.properties.ownerId.properties;
|
|
343
|
+
(0, bun_test_1.expect)(ownerProps).toBeDefined();
|
|
344
|
+
(0, bun_test_1.expect)(Object.keys(ownerProps)).not.toContain("__proto__");
|
|
345
|
+
});
|
|
284
346
|
});
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -36,13 +36,15 @@ import {isValidObjectId} from "./utils";
|
|
|
36
36
|
|
|
37
37
|
export type JSONPrimitive = string | number | boolean | null;
|
|
38
38
|
export interface JSONArray extends Array<JSONValue> {}
|
|
39
|
-
export
|
|
39
|
+
export interface JSONObject {
|
|
40
|
+
[member: string]: JSONValue;
|
|
41
|
+
}
|
|
40
42
|
export type JSONValue = JSONPrimitive | JSONObject | JSONArray;
|
|
41
43
|
|
|
42
|
-
export
|
|
44
|
+
export const addPopulateToQuery = (
|
|
43
45
|
builtQuery: mongoose.Query<any[], any, Record<string, never>, any>,
|
|
44
46
|
populatePaths?: PopulatePath[]
|
|
45
|
-
) {
|
|
47
|
+
) => {
|
|
46
48
|
const paths = populatePaths ?? [];
|
|
47
49
|
let query = builtQuery;
|
|
48
50
|
|
|
@@ -52,7 +54,7 @@ export function addPopulateToQuery(
|
|
|
52
54
|
query = builtQuery.populate({path, select});
|
|
53
55
|
}
|
|
54
56
|
return query;
|
|
55
|
-
}
|
|
57
|
+
};
|
|
56
58
|
|
|
57
59
|
// TODOS:
|
|
58
60
|
// Support bulk actions
|
|
@@ -312,11 +314,11 @@ export interface ModelRouterOptions<T> {
|
|
|
312
314
|
}
|
|
313
315
|
|
|
314
316
|
// Ensures query params are allowed. Also checks nested query params when using $and/$or.
|
|
315
|
-
|
|
317
|
+
const checkQueryParamAllowed = (
|
|
316
318
|
queryParam: string,
|
|
317
319
|
queryParamValue: any,
|
|
318
320
|
queryFields: string[] = []
|
|
319
|
-
) {
|
|
321
|
+
) => {
|
|
320
322
|
// Check the values of each of the complex query params. We don't support recursive queries here,
|
|
321
323
|
// just one level of and/or
|
|
322
324
|
if (COMPLEX_QUERY_PARAMS.includes(queryParam)) {
|
|
@@ -334,7 +336,7 @@ function checkQueryParamAllowed(
|
|
|
334
336
|
title: `${queryParam} is not allowed as a query param.`,
|
|
335
337
|
});
|
|
336
338
|
}
|
|
337
|
-
}
|
|
339
|
+
};
|
|
338
340
|
|
|
339
341
|
// Handles dot notation patches, creates a normal object to be used for updates.
|
|
340
342
|
// function flattenDotNotationPatch(data: any) {
|
|
@@ -358,10 +360,10 @@ function checkQueryParamAllowed(
|
|
|
358
360
|
// Helper to determine if validation should be enabled for a specific operation.
|
|
359
361
|
// When options.validation is not set, returns true — the middleware's own
|
|
360
362
|
// isConfigured check will decide whether to actually validate.
|
|
361
|
-
|
|
363
|
+
const shouldValidate = (
|
|
362
364
|
options: ModelRouterOptions<any>,
|
|
363
365
|
operation: "create" | "update" | "query"
|
|
364
|
-
): boolean {
|
|
366
|
+
): boolean => {
|
|
365
367
|
// Check route-specific validation option first
|
|
366
368
|
if (options.validation !== undefined) {
|
|
367
369
|
if (typeof options.validation === "boolean") {
|
|
@@ -378,14 +380,14 @@ function shouldValidate(
|
|
|
378
380
|
|
|
379
381
|
// Default: let middleware's isConfigured check decide
|
|
380
382
|
return true;
|
|
381
|
-
}
|
|
383
|
+
};
|
|
382
384
|
|
|
383
385
|
// Get body validation middleware if validation is enabled
|
|
384
|
-
|
|
386
|
+
const getBodyValidationMiddleware = <T>(
|
|
385
387
|
model: Model<T>,
|
|
386
388
|
options: ModelRouterOptions<T>,
|
|
387
389
|
operation: "create" | "update"
|
|
388
|
-
): (req: Request, res: Response, next: NextFunction) => void {
|
|
390
|
+
): ((req: Request, res: Response, next: NextFunction) => void) => {
|
|
389
391
|
const validationOptions: import("./openApiValidator").RequestBodyValidatorOptions = {};
|
|
390
392
|
if (!shouldValidate(options, operation)) {
|
|
391
393
|
validationOptions.enabled = false;
|
|
@@ -408,13 +410,13 @@ function getBodyValidationMiddleware<T>(
|
|
|
408
410
|
}
|
|
409
411
|
|
|
410
412
|
return validateModelRequestBody(model, validationOptions);
|
|
411
|
-
}
|
|
413
|
+
};
|
|
412
414
|
|
|
413
415
|
// Get query validation middleware if validation is enabled
|
|
414
|
-
|
|
416
|
+
const getQueryValidationMiddleware = <T>(
|
|
415
417
|
model: Model<T>,
|
|
416
418
|
options: ModelRouterOptions<T>
|
|
417
|
-
): (req: Request, res: Response, next: NextFunction) => void {
|
|
419
|
+
): ((req: Request, res: Response, next: NextFunction) => void) => {
|
|
418
420
|
const querySchema = buildQuerySchemaFromFields(model, options.queryFields);
|
|
419
421
|
const validationOptions: import("./openApiValidator").QueryValidatorOptions = {};
|
|
420
422
|
if (!shouldValidate(options, "query")) {
|
|
@@ -425,7 +427,7 @@ function getQueryValidationMiddleware<T>(
|
|
|
425
427
|
}
|
|
426
428
|
|
|
427
429
|
return validateQueryParams(querySchema, validationOptions);
|
|
428
|
-
}
|
|
430
|
+
};
|
|
429
431
|
|
|
430
432
|
/**
|
|
431
433
|
* Registration object returned by modelRouter when called with a path.
|
package/src/populate.test.ts
CHANGED
|
@@ -196,4 +196,74 @@ describe("getOpenApiSpecForModel edge cases", () => {
|
|
|
196
196
|
});
|
|
197
197
|
expect(result.properties).toBeDefined();
|
|
198
198
|
});
|
|
199
|
+
|
|
200
|
+
it("uses openApiComponent $ref when provided", () => {
|
|
201
|
+
const result = getOpenApiSpecForModel(FoodModel, {
|
|
202
|
+
populatePaths: [{openApiComponent: "UserComponent", path: "ownerId"}],
|
|
203
|
+
});
|
|
204
|
+
expect(result.properties.ownerId).toEqual({
|
|
205
|
+
$ref: "#/components/schemas/UserComponent",
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("populates array ref fields (eatenBy)", () => {
|
|
210
|
+
const result = getOpenApiSpecForModel(FoodModel, {
|
|
211
|
+
populatePaths: [{path: "eatenBy"}],
|
|
212
|
+
});
|
|
213
|
+
expect(result.properties.eatenBy).toBeDefined();
|
|
214
|
+
expect((result.properties.eatenBy as any).items).toBeDefined();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("populates nested ref in sub-schema (likesIds.userId)", () => {
|
|
218
|
+
const result = getOpenApiSpecForModel(FoodModel, {
|
|
219
|
+
populatePaths: [{path: "likesIds.userId"}],
|
|
220
|
+
});
|
|
221
|
+
expect(result.properties.likesIds).toBeDefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("includes virtuals from model schema", () => {
|
|
225
|
+
const result = getOpenApiSpecForModel(FoodModel);
|
|
226
|
+
expect(result.properties.description).toBeDefined();
|
|
227
|
+
expect((result.properties.description as any).type).toBe("any");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("includes virtuals from child schemas", () => {
|
|
231
|
+
const childSub = new Schema({amount: {description: "Amount", type: Number}});
|
|
232
|
+
childSub.virtual("displayAmount").get(function () {
|
|
233
|
+
return `${this.amount} units`;
|
|
234
|
+
});
|
|
235
|
+
const parentSchema = new Schema({
|
|
236
|
+
detail: {description: "Single embedded detail", type: childSub},
|
|
237
|
+
title: {description: "Title", type: String},
|
|
238
|
+
});
|
|
239
|
+
const ParentModel =
|
|
240
|
+
mongoose.models.ParentWithChildVirtual ||
|
|
241
|
+
mongoose.model("ParentWithChildVirtual", parentSchema);
|
|
242
|
+
const result = getOpenApiSpecForModel(ParentModel);
|
|
243
|
+
const detail = result.properties.detail as any;
|
|
244
|
+
expect(detail.properties.displayAmount).toBeDefined();
|
|
245
|
+
expect(detail.properties.displayAmount.type).toBe("any");
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("filterKeys (via getOpenApiSpecForModel populatePaths)", () => {
|
|
250
|
+
it("filters populated fields using dot-notation keys", () => {
|
|
251
|
+
const result = getOpenApiSpecForModel(FoodModel, {
|
|
252
|
+
populatePaths: [{fields: ["name.nested"], path: "ownerId"}],
|
|
253
|
+
});
|
|
254
|
+
const ownerProps = (result.properties.ownerId as any).properties;
|
|
255
|
+
expect(ownerProps.name).toBeDefined();
|
|
256
|
+
expect(typeof ownerProps.name).toBe("object");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("rejects prototype pollution keys in nested dot-notation", () => {
|
|
260
|
+
const result = getOpenApiSpecForModel(FoodModel, {
|
|
261
|
+
populatePaths: [{fields: ["__proto__.polluted"], path: "ownerId"}],
|
|
262
|
+
});
|
|
263
|
+
expect(result.properties).toBeDefined();
|
|
264
|
+
expect((Object.prototype as any).polluted).toBeUndefined();
|
|
265
|
+
const ownerProps = (result.properties.ownerId as any).properties;
|
|
266
|
+
expect(ownerProps).toBeDefined();
|
|
267
|
+
expect(Object.keys(ownerProps)).not.toContain("__proto__");
|
|
268
|
+
});
|
|
199
269
|
});
|