mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-17 01:17:29 +02:00
refactor: improve OpenAPI refs (#1620)
* refactor: simplify FeatureEnvironmentSchema name * refactor: format schema files * fix: pass nested schemas to FromSchema * refactor: remove ref order note * refactor: fix overly strict required fields * refactor: clean up mapper names and paths * refactor: replace mappers with optional fields
This commit is contained in:
parent
d4581a1ae2
commit
59060ed3ea
@ -10,7 +10,7 @@ import { tagsResponseSchema } from './spec/tags-response-schema';
|
||||
import { createStrategySchema } from './spec/create-strategy-schema';
|
||||
import { featureSchema } from './spec/feature-schema';
|
||||
import { parametersSchema } from './spec/parameters-schema';
|
||||
import { featureEnvironmentInfoSchema } from './spec/feature-environment-info-schema';
|
||||
import { featureEnvironmentSchema } from './spec/feature-environment-schema';
|
||||
import { emptyResponseSchema } from './spec/empty-response-schema';
|
||||
import { patchOperationSchema } from './spec/patch-operation-schema';
|
||||
import { updateFeatureSchema } from './spec/updateFeatureSchema';
|
||||
@ -48,7 +48,7 @@ export const createOpenApiSchema = (
|
||||
createStrategySchema,
|
||||
featureSchema,
|
||||
featuresSchema,
|
||||
featureEnvironmentInfoSchema,
|
||||
featureEnvironmentSchema,
|
||||
featureStrategySchema,
|
||||
emptyResponseSchema,
|
||||
overrideSchema,
|
||||
|
@ -1,29 +0,0 @@
|
||||
import { SchemaMapper } from './mapper';
|
||||
import { IFeatureEnvironmentInfo } from '../../types/model';
|
||||
import { FeatureEnvironmentInfoSchema } from '../spec/feature-environment-info-schema';
|
||||
import { FeatureStrategyMapper } from './feature-strategy.mapper';
|
||||
|
||||
export class EnvironmentInfoMapper
|
||||
implements
|
||||
SchemaMapper<
|
||||
FeatureEnvironmentInfoSchema,
|
||||
IFeatureEnvironmentInfo,
|
||||
Partial<FeatureEnvironmentInfoSchema>
|
||||
>
|
||||
{
|
||||
private mapper = new FeatureStrategyMapper();
|
||||
|
||||
fromPublic(input: FeatureEnvironmentInfoSchema): IFeatureEnvironmentInfo {
|
||||
return {
|
||||
...input,
|
||||
strategies: input.strategies.map(this.mapper.fromPublic),
|
||||
};
|
||||
}
|
||||
|
||||
toPublic(input: IFeatureEnvironmentInfo): FeatureEnvironmentInfoSchema {
|
||||
return {
|
||||
...input,
|
||||
strategies: input.strategies.map(this.mapper.toPublic),
|
||||
};
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import { SchemaMapper } from './mapper';
|
||||
import { IFeatureStrategy } from '../../types/model';
|
||||
import { CreateStrategySchema } from '../spec/create-strategy-schema';
|
||||
import { UpdateStrategySchema } from '../spec/update-strategy-schema';
|
||||
import { FeatureStrategySchema } from '../spec/feature-strategy-schema';
|
||||
|
||||
export class FeatureStrategyMapper
|
||||
implements
|
||||
SchemaMapper<
|
||||
FeatureStrategySchema,
|
||||
IFeatureStrategy,
|
||||
CreateStrategySchema | UpdateStrategySchema
|
||||
>
|
||||
{
|
||||
fromPublic(input: FeatureStrategySchema): IFeatureStrategy {
|
||||
return {
|
||||
...input,
|
||||
id: input.id || '',
|
||||
projectId: input.projectId! || '',
|
||||
};
|
||||
}
|
||||
|
||||
toPublic(input: IFeatureStrategy): FeatureStrategySchema {
|
||||
return {
|
||||
...input,
|
||||
name: input.strategyName,
|
||||
};
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export * from './environment-info.mapper';
|
||||
export * from './feature-strategy.mapper';
|
||||
export * from './strategy.mapper';
|
@ -1,7 +0,0 @@
|
||||
// Convert between public schema types and internal data types.
|
||||
// Avoids coupling public schemas to internal implementation details.
|
||||
export interface SchemaMapper<SCHEMA, INTERNAL, INPUT = Partial<SCHEMA>> {
|
||||
fromPublic(input: SCHEMA): INTERNAL;
|
||||
toPublic(input: INTERNAL): SCHEMA;
|
||||
mapInput?(input: INPUT): INTERNAL;
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import { SchemaMapper } from './mapper';
|
||||
import { IStrategyConfig } from '../../types/model';
|
||||
import { StrategySchema } from '../spec/strategy-schema';
|
||||
import { CreateStrategySchema } from '../spec/create-strategy-schema';
|
||||
import { UpdateStrategySchema } from '../spec/update-strategy-schema';
|
||||
|
||||
export class StrategyMapper
|
||||
implements
|
||||
SchemaMapper<
|
||||
StrategySchema,
|
||||
IStrategyConfig,
|
||||
CreateStrategySchema | UpdateStrategySchema
|
||||
>
|
||||
{
|
||||
fromPublic(input: StrategySchema): IStrategyConfig {
|
||||
return input;
|
||||
}
|
||||
|
||||
toPublic(input: IStrategyConfig): StrategySchema {
|
||||
return input;
|
||||
}
|
||||
|
||||
mapInput(
|
||||
input: CreateStrategySchema | UpdateStrategySchema,
|
||||
): IStrategyConfig {
|
||||
return {
|
||||
...input,
|
||||
name: input.name || '',
|
||||
parameters: input.parameters || {},
|
||||
constraints: input.constraints || [],
|
||||
};
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import { constraintSchema } from './constraint-schema';
|
||||
const schema = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['name'],
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
@ -14,7 +15,9 @@ const schema = {
|
||||
},
|
||||
constraints: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/constraintSchema' },
|
||||
items: {
|
||||
$ref: '#/components/schemas/constraintSchema',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
$ref: '#/components/schemas/parametersSchema',
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const featureEnvironmentInfoResponse: OpenAPIV3.ResponseObject = {
|
||||
description: 'featureEnvironmentInfoResponse',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/featureEnvironmentInfoSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
12
src/lib/openapi/spec/feature-environment-response.ts
Normal file
12
src/lib/openapi/spec/feature-environment-response.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
|
||||
export const featureEnvironmentResponse: OpenAPIV3.ResponseObject = {
|
||||
description: 'featureEnvironmentResponse',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/featureEnvironmentSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -1,10 +1,12 @@
|
||||
import { createSchemaObject, CreateSchemaType } from '../types';
|
||||
import { featureStrategySchema } from './feature-strategy-schema';
|
||||
import { constraintSchema } from './constraint-schema';
|
||||
import { parametersSchema } from './parameters-schema';
|
||||
|
||||
let schema = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['name', 'environment', 'enabled', 'strategies'],
|
||||
required: ['name', 'enabled'],
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
@ -27,9 +29,11 @@ let schema = {
|
||||
},
|
||||
'components/schemas': {
|
||||
featureStrategySchema,
|
||||
constraintSchema,
|
||||
parametersSchema,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type FeatureEnvironmentInfoSchema = CreateSchemaType<typeof schema>;
|
||||
export type FeatureEnvironmentSchema = CreateSchemaType<typeof schema>;
|
||||
|
||||
export const featureEnvironmentInfoSchema = createSchemaObject(schema);
|
||||
export const featureEnvironmentSchema = createSchemaObject(schema);
|
@ -1,12 +1,16 @@
|
||||
import { createSchemaObject, CreateSchemaType } from '../types';
|
||||
import { strategySchema } from './strategy-schema';
|
||||
import { variantSchema } from './variant-schema';
|
||||
import { featureEnvironmentInfoSchema } from './feature-environment-info-schema';
|
||||
import { featureEnvironmentSchema } from './feature-environment-schema';
|
||||
import { featureStrategySchema } from './feature-strategy-schema';
|
||||
import { constraintSchema } from './constraint-schema';
|
||||
import { parametersSchema } from './parameters-schema';
|
||||
import { overrideSchema } from './override-schema';
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['name', 'project'],
|
||||
required: ['name'],
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
@ -45,12 +49,14 @@ const schema = {
|
||||
environments: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/featureEnvironmentInfoSchema',
|
||||
$ref: '#/components/schemas/featureEnvironmentSchema',
|
||||
},
|
||||
},
|
||||
strategies: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/strategySchema' },
|
||||
items: {
|
||||
$ref: '#/components/schemas/strategySchema',
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
type: 'array',
|
||||
@ -60,7 +66,11 @@ const schema = {
|
||||
},
|
||||
},
|
||||
'components/schemas': {
|
||||
featureEnvironmentInfoSchema,
|
||||
constraintSchema,
|
||||
featureEnvironmentSchema,
|
||||
featureStrategySchema,
|
||||
overrideSchema,
|
||||
parametersSchema,
|
||||
strategySchema,
|
||||
variantSchema,
|
||||
},
|
||||
|
@ -7,7 +7,6 @@ export const schema = {
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
'id',
|
||||
'name',
|
||||
'featureName',
|
||||
'strategyName',
|
||||
'constraints',
|
||||
@ -43,9 +42,13 @@ export const schema = {
|
||||
},
|
||||
constraints: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/constraintSchema' },
|
||||
items: {
|
||||
$ref: '#/components/schemas/constraintSchema',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
$ref: '#/components/schemas/parametersSchema',
|
||||
},
|
||||
parameters: { $ref: '#/components/schemas/parametersSchema' },
|
||||
},
|
||||
'components/schemas': {
|
||||
constraintSchema,
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { createSchemaObject, CreateSchemaType } from '../types';
|
||||
import { featureSchema } from './feature-schema';
|
||||
import { parametersSchema } from './parameters-schema';
|
||||
import { variantSchema } from './variant-schema';
|
||||
import { overrideSchema } from './override-schema';
|
||||
import { featureEnvironmentSchema } from './feature-environment-schema';
|
||||
import { featureStrategySchema } from './feature-strategy-schema';
|
||||
import { constraintSchema } from './constraint-schema';
|
||||
import { strategySchema } from './strategy-schema';
|
||||
|
||||
const schema = {
|
||||
type: 'object',
|
||||
@ -11,11 +18,20 @@ const schema = {
|
||||
},
|
||||
features: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/featureSchema' },
|
||||
items: {
|
||||
$ref: '#/components/schemas/featureSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
'components/schemas': {
|
||||
featureSchema: { schema: featureSchema },
|
||||
featureSchema,
|
||||
constraintSchema,
|
||||
featureEnvironmentSchema,
|
||||
featureStrategySchema,
|
||||
overrideSchema,
|
||||
parametersSchema,
|
||||
strategySchema,
|
||||
variantSchema,
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { parametersSchema } from './parameters-schema';
|
||||
export const strategySchemaDefinition = {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['name', 'constraints', 'parameters'],
|
||||
required: ['name'],
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
@ -18,9 +18,13 @@ export const strategySchemaDefinition = {
|
||||
},
|
||||
constraints: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/constraintSchema' },
|
||||
items: {
|
||||
$ref: '#/components/schemas/constraintSchema',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
$ref: '#/components/schemas/parametersSchema',
|
||||
},
|
||||
parameters: { $ref: '#/components/schemas/parametersSchema' },
|
||||
},
|
||||
'components/schemas': {
|
||||
constraintSchema,
|
||||
|
@ -29,7 +29,9 @@ const schema = {
|
||||
},
|
||||
constraints: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/constraintSchema' },
|
||||
items: {
|
||||
$ref: '#/components/schemas/constraintSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
'components/schemas': {
|
||||
|
@ -32,7 +32,9 @@ const schema = {
|
||||
},
|
||||
overrides: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/overrideSchema' },
|
||||
items: {
|
||||
$ref: '#/components/schemas/overrideSchema',
|
||||
},
|
||||
},
|
||||
},
|
||||
'components/schemas': {
|
||||
|
@ -28,7 +28,6 @@ export type CreateSchemaType<T> = FromSchema<
|
||||
// Create an OpenAPIV3.SchemaObject from a const schema object.
|
||||
// Make sure the schema contains an object of refs for type generation.
|
||||
// Pass an empty 'components/schemas' object if there are no refs in the schema.
|
||||
// Note: The order of the refs must match the order they are present in the object
|
||||
export const createSchemaObject = <
|
||||
T extends { 'components/schemas': { [key: string]: object } },
|
||||
>(
|
||||
|
@ -24,7 +24,7 @@ import { FeatureSchema } from '../../../openapi/spec/feature-schema';
|
||||
import { createStrategyRequest } from '../../../openapi/spec/create-strategy-request';
|
||||
import { StrategySchema } from '../../../openapi/spec/strategy-schema';
|
||||
import { featuresResponse } from '../../../openapi/spec/features-response';
|
||||
import { featureEnvironmentInfoResponse } from '../../../openapi/spec/feature-environment-info-response';
|
||||
import { featureEnvironmentResponse } from '../../../openapi/spec/feature-environment-response';
|
||||
import { strategiesResponse } from '../../../openapi/spec/strategies-response';
|
||||
import { strategyResponse } from '../../../openapi/spec/strategy-response';
|
||||
import { emptyResponse } from '../../../openapi/spec/empty-response';
|
||||
@ -32,16 +32,12 @@ import { updateFeatureRequest } from '../../../openapi/spec/update-feature-reque
|
||||
import { patchRequest } from '../../../openapi/spec/patch-request';
|
||||
import { updateStrategyRequest } from '../../../openapi/spec/update-strategy-request';
|
||||
import { cloneFeatureRequest } from '../../../openapi/spec/clone-feature-request';
|
||||
import { FeatureEnvironmentInfoSchema } from '../../../openapi/spec/feature-environment-info-schema';
|
||||
import { FeatureEnvironmentSchema } from '../../../openapi/spec/feature-environment-schema';
|
||||
import { ParametersSchema } from '../../../openapi/spec/parameters-schema';
|
||||
import { FeaturesSchema } from '../../../openapi/spec/features-schema';
|
||||
import { UpdateFeatureSchema } from '../../../openapi/spec/updateFeatureSchema';
|
||||
import { UpdateStrategySchema } from '../../../openapi/spec/update-strategy-schema';
|
||||
import { CreateStrategySchema } from '../../../openapi/spec/create-strategy-schema';
|
||||
import {
|
||||
EnvironmentInfoMapper,
|
||||
StrategyMapper,
|
||||
} from '../../../openapi/mappers';
|
||||
|
||||
interface FeatureStrategyParams {
|
||||
projectId: string;
|
||||
@ -77,11 +73,6 @@ type ProjectFeaturesServices = Pick<
|
||||
export default class ProjectFeaturesController extends Controller {
|
||||
private featureService: FeatureToggleService;
|
||||
|
||||
private strategyMapper: StrategyMapper = new StrategyMapper();
|
||||
|
||||
private environmentMapper: EnvironmentInfoMapper =
|
||||
new EnvironmentInfoMapper();
|
||||
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
@ -102,7 +93,7 @@ export default class ProjectFeaturesController extends Controller {
|
||||
openApiService.validPath({
|
||||
tags: ['admin'],
|
||||
operationId: 'getEnvironment',
|
||||
responses: { 200: featureEnvironmentInfoResponse },
|
||||
responses: { 200: featureEnvironmentResponse },
|
||||
}),
|
||||
],
|
||||
});
|
||||
@ -448,7 +439,7 @@ export default class ProjectFeaturesController extends Controller {
|
||||
|
||||
async getEnvironment(
|
||||
req: Request<FeatureStrategyParams, any, any, any>,
|
||||
res: Response<FeatureEnvironmentInfoSchema>,
|
||||
res: Response<FeatureEnvironmentSchema>,
|
||||
): Promise<void> {
|
||||
const { environment, featureName, projectId } = req.params;
|
||||
const environmentInfo = await this.featureService.getEnvironmentInfo(
|
||||
@ -456,7 +447,7 @@ export default class ProjectFeaturesController extends Controller {
|
||||
environment,
|
||||
featureName,
|
||||
);
|
||||
res.status(200).json(this.environmentMapper.toPublic(environmentInfo));
|
||||
res.status(200).json(environmentInfo);
|
||||
}
|
||||
|
||||
async toggleEnvironmentOn(
|
||||
@ -496,11 +487,11 @@ export default class ProjectFeaturesController extends Controller {
|
||||
const { projectId, featureName, environment } = req.params;
|
||||
const userName = extractUsername(req);
|
||||
const strategy = await this.featureService.createStrategy(
|
||||
this.strategyMapper.mapInput(req.body),
|
||||
req.body,
|
||||
{ environment, projectId, featureName },
|
||||
userName,
|
||||
);
|
||||
res.status(200).json(this.strategyMapper.toPublic(strategy));
|
||||
res.status(200).json(strategy);
|
||||
}
|
||||
|
||||
async getStrategies(
|
||||
@ -514,9 +505,7 @@ export default class ProjectFeaturesController extends Controller {
|
||||
featureName,
|
||||
environment,
|
||||
);
|
||||
res.status(200).json(
|
||||
featureStrategies.map(this.strategyMapper.toPublic),
|
||||
);
|
||||
res.status(200).json(featureStrategies);
|
||||
}
|
||||
|
||||
async updateStrategy(
|
||||
@ -531,7 +520,7 @@ export default class ProjectFeaturesController extends Controller {
|
||||
{ environment, projectId, featureName },
|
||||
userName,
|
||||
);
|
||||
res.status(200).json(this.strategyMapper.fromPublic(updatedStrategy));
|
||||
res.status(200).json(updatedStrategy);
|
||||
}
|
||||
|
||||
async patchStrategy(
|
||||
@ -549,7 +538,7 @@ export default class ProjectFeaturesController extends Controller {
|
||||
{ environment, projectId, featureName },
|
||||
userName,
|
||||
);
|
||||
res.status(200).json(this.strategyMapper.toPublic(updatedStrategy));
|
||||
res.status(200).json(updatedStrategy);
|
||||
}
|
||||
|
||||
async getStrategy(
|
||||
@ -560,7 +549,7 @@ export default class ProjectFeaturesController extends Controller {
|
||||
const { strategyId } = req.params;
|
||||
this.logger.info(strategyId);
|
||||
const strategy = await this.featureService.getStrategy(strategyId);
|
||||
res.status(200).json(this.strategyMapper.toPublic(strategy));
|
||||
res.status(200).json(strategy);
|
||||
}
|
||||
|
||||
async deleteStrategy(
|
||||
@ -601,7 +590,7 @@ export default class ProjectFeaturesController extends Controller {
|
||||
{ environment, projectId, featureName },
|
||||
userName,
|
||||
);
|
||||
res.status(200).json(this.strategyMapper.toPublic(updatedStrategy));
|
||||
res.status(200).json(updatedStrategy);
|
||||
}
|
||||
|
||||
async getStrategyParameters(
|
||||
|
@ -22,8 +22,8 @@ export enum WeightType {
|
||||
export interface IStrategyConfig {
|
||||
id?: string;
|
||||
name: string;
|
||||
constraints: IConstraint[];
|
||||
parameters: { [key: string]: string };
|
||||
constraints?: IConstraint[];
|
||||
parameters?: { [key: string]: string };
|
||||
sortOrder?: number;
|
||||
}
|
||||
export interface IFeatureStrategy {
|
||||
|
@ -152,13 +152,16 @@ Object {
|
||||
"type": "number",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"name",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"emptyResponseSchema": Object {
|
||||
"description": "OK",
|
||||
"type": "object",
|
||||
},
|
||||
"featureEnvironmentInfoSchema": Object {
|
||||
"featureEnvironmentSchema": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"enabled": Object {
|
||||
@ -182,9 +185,7 @@ Object {
|
||||
},
|
||||
"required": Array [
|
||||
"name",
|
||||
"environment",
|
||||
"enabled",
|
||||
"strategies",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
@ -207,7 +208,7 @@ Object {
|
||||
},
|
||||
"environments": Object {
|
||||
"items": Object {
|
||||
"$ref": "#/components/schemas/featureEnvironmentInfoSchema",
|
||||
"$ref": "#/components/schemas/featureEnvironmentSchema",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
@ -246,7 +247,6 @@ Object {
|
||||
},
|
||||
"required": Array [
|
||||
"name",
|
||||
"project",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
@ -291,7 +291,6 @@ Object {
|
||||
},
|
||||
"required": Array [
|
||||
"id",
|
||||
"name",
|
||||
"featureName",
|
||||
"strategyName",
|
||||
"constraints",
|
||||
@ -394,8 +393,6 @@ Object {
|
||||
},
|
||||
"required": Array [
|
||||
"name",
|
||||
"constraints",
|
||||
"parameters",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
@ -1078,11 +1075,11 @@ Object {
|
||||
"content": Object {
|
||||
"application/json": Object {
|
||||
"schema": Object {
|
||||
"$ref": "#/components/schemas/featureEnvironmentInfoSchema",
|
||||
"$ref": "#/components/schemas/featureEnvironmentSchema",
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "featureEnvironmentInfoResponse",
|
||||
"description": "featureEnvironmentResponse",
|
||||
},
|
||||
},
|
||||
"tags": Array [
|
||||
|
Loading…
Reference in New Issue
Block a user