1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-12 01:17:04 +02:00

validation: fix _some_ 201 created location header endpoints

This commit is contained in:
Thomas Heartman 2022-09-13 20:23:51 +02:00
parent d0500b6c1a
commit fe07191c63
12 changed files with 114 additions and 30 deletions

View File

@ -4,6 +4,7 @@ import {
IContextField, IContextField,
IContextFieldDto, IContextFieldDto,
IContextFieldStore, IContextFieldStore,
ILegalValue,
} from '../types/stores/context-field-store'; } from '../types/stores/context-field-store';
const COLUMNS = [ const COLUMNS = [
@ -16,7 +17,16 @@ const COLUMNS = [
]; ];
const TABLE = 'context_fields'; const TABLE = 'context_fields';
const mapRow: (object) => IContextField = (row) => ({ type ContextFieldDB = {
name: string;
description: string;
stickiness: boolean;
sort_order: number;
legal_values: ILegalValue[];
created_at: Date;
};
const mapRow = (row: ContextFieldDB): IContextField => ({
name: row.name, name: row.name,
description: row.description, description: row.description,
stickiness: row.stickiness, stickiness: row.stickiness,
@ -88,15 +98,17 @@ class ContextFieldStore implements IContextFieldStore {
return present; return present;
} }
// TODO: write tests for the changes you made here?
async create(contextField: IContextFieldDto): Promise<IContextField> { async create(contextField: IContextFieldDto): Promise<IContextField> {
const row = await this.db(TABLE) const [row] = await this.db(TABLE)
.insert(this.fieldToRow(contextField)) .insert(this.fieldToRow(contextField))
.returning('*'); .returning('*');
return mapRow(row); return mapRow(row);
} }
async update(data: IContextFieldDto): Promise<IContextField> { async update(data: IContextFieldDto): Promise<IContextField> {
const row = await this.db(TABLE) const [row] = await this.db(TABLE)
.where({ name: data.name }) .where({ name: data.name })
.update(this.fieldToRow(data)) .update(this.fieldToRow(data))
.returning('*'); .returning('*');

View File

@ -14,3 +14,27 @@ export const createResponseSchema = (
}, },
}; };
}; };
export const resourceCreatedResponseSchema = (
schemaName: string,
): OpenAPIV3.ResponseObject => {
return {
headers: {
location: {
description: 'The location of the newly created resource.',
schema: {
type: 'string',
format: 'uri',
},
},
},
description: `The resource was successfully created.`,
content: {
'application/json': {
schema: {
$ref: `#/components/schemas/${schemaName}`,
},
},
},
};
};

View File

@ -14,7 +14,7 @@ const ajv = new Ajv({
), ),
}); });
addFormats(ajv, ['date-time']); addFormats(ajv, ['date-time', 'uri', 'uri-reference']);
// example was superseded by examples in openapi 3.1, but we're still on 3.0, so // example was superseded by examples in openapi 3.1, but we're still on 3.0, so
// let's add it back in! // let's add it back in!

View File

@ -19,7 +19,10 @@ import { createApiToken } from '../../schema/api-token-schema';
import { OpenApiService } from '../../services/openapi-service'; import { OpenApiService } from '../../services/openapi-service';
import { IUnleashServices } from '../../types'; import { IUnleashServices } from '../../types';
import { createRequestSchema } from '../../openapi/util/create-request-schema'; import { createRequestSchema } from '../../openapi/util/create-request-schema';
import { createResponseSchema } from '../../openapi/util/create-response-schema'; import {
createResponseSchema,
resourceCreatedResponseSchema,
} from '../../openapi/util/create-response-schema';
import { import {
apiTokensSchema, apiTokensSchema,
ApiTokensSchema, ApiTokensSchema,
@ -96,7 +99,7 @@ export class ApiTokenController extends Controller {
operationId: 'createApiToken', operationId: 'createApiToken',
requestBody: createRequestSchema('createApiTokenSchema'), requestBody: createRequestSchema('createApiTokenSchema'),
responses: { responses: {
201: createResponseSchema('apiTokenSchema'), 201: resourceCreatedResponseSchema('apiTokenSchema'),
}, },
}), }),
], ],

View File

@ -24,7 +24,10 @@ import {
import { ContextFieldsSchema } from '../../openapi/spec/context-fields-schema'; import { ContextFieldsSchema } from '../../openapi/spec/context-fields-schema';
import { UpsertContextFieldSchema } from '../../openapi/spec/upsert-context-field-schema'; import { UpsertContextFieldSchema } from '../../openapi/spec/upsert-context-field-schema';
import { createRequestSchema } from '../../openapi/util/create-request-schema'; import { createRequestSchema } from '../../openapi/util/create-request-schema';
import { createResponseSchema } from '../../openapi/util/create-response-schema'; import {
createResponseSchema,
resourceCreatedResponseSchema,
} from '../../openapi/util/create-response-schema';
import { serializeDates } from '../../types/serialize-dates'; import { serializeDates } from '../../types/serialize-dates';
import NotFoundError from '../../error/notfound-error'; import NotFoundError from '../../error/notfound-error';
import { NameSchema } from '../../openapi/spec/name-schema'; import { NameSchema } from '../../openapi/spec/name-schema';
@ -98,7 +101,9 @@ export class ContextController extends Controller {
'upsertContextFieldSchema', 'upsertContextFieldSchema',
), ),
responses: { responses: {
201: emptyResponse, 201: resourceCreatedResponseSchema(
'contextFieldSchema',
),
}, },
}), }),
], ],
@ -189,13 +194,19 @@ export class ContextController extends Controller {
async createContextField( async createContextField(
req: IAuthRequest<void, void, UpsertContextFieldSchema>, req: IAuthRequest<void, void, UpsertContextFieldSchema>,
res: Response, res: Response<ContextFieldSchema>,
): Promise<void> { ): Promise<void> {
const value = req.body; const value = req.body;
const userName = extractUsername(req); const userName = extractUsername(req);
await this.contextService.createContextField(value, userName); const result = await this.contextService.createContextField(
res.status(201).end(); value,
userName,
);
res.status(201)
.header('location', `context/${result.name}`) // todo: how to ensure that the location is (and stays) correct?
.json(serializeDates(result))
.end();
} }
async updateContextField( async updateContextField(

View File

@ -25,7 +25,10 @@ import { TagsSchema } from '../../openapi/spec/tags-schema';
import { serializeDates } from '../../types/serialize-dates'; import { serializeDates } from '../../types/serialize-dates';
import { OpenApiService } from '../../services/openapi-service'; import { OpenApiService } from '../../services/openapi-service';
import { createRequestSchema } from '../../openapi/util/create-request-schema'; import { createRequestSchema } from '../../openapi/util/create-request-schema';
import { createResponseSchema } from '../../openapi/util/create-response-schema'; import {
createResponseSchema,
resourceCreatedResponseSchema,
} from '../../openapi/util/create-response-schema';
import { emptyResponse } from '../../openapi/util/standard-responses'; import { emptyResponse } from '../../openapi/util/standard-responses';
const version = 1; const version = 1;
@ -123,7 +126,9 @@ class FeatureController extends Controller {
tags: ['Features'], tags: ['Features'],
operationId: 'addTag', operationId: 'addTag',
requestBody: createRequestSchema('tagSchema'), requestBody: createRequestSchema('tagSchema'),
responses: { 201: createResponseSchema('tagSchema') }, responses: {
201: resourceCreatedResponseSchema('tagSchema'),
},
}), }),
], ],
}); });
@ -221,7 +226,7 @@ class FeatureController extends Controller {
req.body, req.body,
userName, userName,
); );
res.status(201).json(tag); res.status(201).header('location', `${featureName}/tags`).json(tag);
} }
// TODO // TODO

View File

@ -15,7 +15,10 @@ import { IAuthRequest } from '../unleash-types';
import { OpenApiService } from '../../services/openapi-service'; import { OpenApiService } from '../../services/openapi-service';
import { emptyResponse } from '../../openapi/util/standard-responses'; import { emptyResponse } from '../../openapi/util/standard-responses';
import { createRequestSchema } from '../../openapi/util/create-request-schema'; import { createRequestSchema } from '../../openapi/util/create-request-schema';
import { createResponseSchema } from '../../openapi/util/create-response-schema'; import {
createResponseSchema,
resourceCreatedResponseSchema,
} from '../../openapi/util/create-response-schema';
import { import {
strategySchema, strategySchema,
StrategySchema, StrategySchema,
@ -102,7 +105,9 @@ class StrategyController extends Controller {
tags: ['Strategies'], tags: ['Strategies'],
operationId: 'createStrategy', operationId: 'createStrategy',
requestBody: createRequestSchema('upsertStrategySchema'), requestBody: createRequestSchema('upsertStrategySchema'),
responses: { 201: emptyResponse }, responses: {
201: resourceCreatedResponseSchema('strategySchema'),
},
}), }),
], ],
}); });
@ -193,12 +198,18 @@ class StrategyController extends Controller {
async createStrategy( async createStrategy(
req: IAuthRequest<unknown, UpsertStrategySchema>, req: IAuthRequest<unknown, UpsertStrategySchema>,
res: Response<void>, res: Response<StrategySchema>,
): Promise<void> { ): Promise<void> {
const userName = extractUsername(req); const userName = extractUsername(req);
await this.strategyService.createStrategy(req.body, userName); const strategy = await this.strategyService.createStrategy(
res.status(201).end(); req.body,
userName,
);
res.header('location', `strategies/${strategy.name}`)
.status(201)
.json(strategy)
.end();
} }
async updateStrategy( async updateStrategy(

View File

@ -13,7 +13,10 @@ import TagTypeService from '../../services/tag-type-service';
import { Logger } from '../../logger'; import { Logger } from '../../logger';
import { IAuthRequest } from '../unleash-types'; import { IAuthRequest } from '../unleash-types';
import { createRequestSchema } from '../../openapi/util/create-request-schema'; import { createRequestSchema } from '../../openapi/util/create-request-schema';
import { createResponseSchema } from '../../openapi/util/create-response-schema'; import {
createResponseSchema,
resourceCreatedResponseSchema,
} from '../../openapi/util/create-response-schema';
import { TagTypesSchema } from '../../openapi/spec/tag-types-schema'; import { TagTypesSchema } from '../../openapi/spec/tag-types-schema';
import { ValidateTagTypeSchema } from '../../openapi/spec/validate-tag-type-schema'; import { ValidateTagTypeSchema } from '../../openapi/spec/validate-tag-type-schema';
import { import {
@ -66,7 +69,9 @@ class TagTypeController extends Controller {
openApiService.validPath({ openApiService.validPath({
tags: ['Tags'], tags: ['Tags'],
operationId: 'createTagType', operationId: 'createTagType',
responses: { 201: createResponseSchema('tagTypeSchema') }, responses: {
201: resourceCreatedResponseSchema('tagTypeSchema'),
},
requestBody: createRequestSchema('tagTypeSchema'), requestBody: createRequestSchema('tagTypeSchema'),
}), }),
], ],

View File

@ -10,7 +10,10 @@ import { NONE, UPDATE_FEATURE } from '../../types/permissions';
import { extractUsername } from '../../util/extract-user'; import { extractUsername } from '../../util/extract-user';
import { IAuthRequest } from '../unleash-types'; import { IAuthRequest } from '../unleash-types';
import { createRequestSchema } from '../../openapi/util/create-request-schema'; import { createRequestSchema } from '../../openapi/util/create-request-schema';
import { createResponseSchema } from '../../openapi/util/create-response-schema'; import {
createResponseSchema,
resourceCreatedResponseSchema,
} from '../../openapi/util/create-response-schema';
import { tagsSchema, TagsSchema } from '../../openapi/spec/tags-schema'; import { tagsSchema, TagsSchema } from '../../openapi/spec/tags-schema';
import { TagSchema } from '../../openapi/spec/tag-schema'; import { TagSchema } from '../../openapi/spec/tag-schema';
import { OpenApiService } from '../../services/openapi-service'; import { OpenApiService } from '../../services/openapi-service';
@ -64,7 +67,9 @@ class TagController extends Controller {
tags: ['Tags'], tags: ['Tags'],
operationId: 'createTag', operationId: 'createTag',
responses: { responses: {
201: emptyResponse, 201: resourceCreatedResponseSchema(
'tagWithVersionSchema',
),
}, },
requestBody: createRequestSchema('tagSchema'), requestBody: createRequestSchema('tagSchema'),
}), }),
@ -157,11 +162,14 @@ class TagController extends Controller {
async createTag( async createTag(
req: IAuthRequest<unknown, unknown, TagSchema>, req: IAuthRequest<unknown, unknown, TagSchema>,
res: Response, res: Response<TagWithVersionSchema>,
): Promise<void> { ): Promise<void> {
const userName = extractUsername(req); const userName = extractUsername(req);
await this.tagService.createTag(req.body, userName); const tag = await this.tagService.createTag(req.body, userName);
res.status(201).end(); res.status(201)
.header('location', `tags/${tag.type}/${tag.value}`)
.json({ version, tag })
.end();
} }
async deleteTag( async deleteTag(

View File

@ -55,18 +55,20 @@ class ContextService {
async createContextField( async createContextField(
value: IContextFieldDto, value: IContextFieldDto,
userName: string, userName: string,
): Promise<void> { ): Promise<IContextField> {
// validations // validations
await this.validateUniqueName(value); await this.validateUniqueName(value);
const contextField = await contextSchema.validateAsync(value); const contextField = await contextSchema.validateAsync(value);
// creations // creations
await this.contextFieldStore.create(value); const createdField = await this.contextFieldStore.create(value);
await this.eventStore.store({ await this.eventStore.store({
type: CONTEXT_FIELD_CREATED, type: CONTEXT_FIELD_CREATED,
createdBy: userName, createdBy: userName,
data: contextField, data: contextField,
}); });
return createdField;
} }
async updateContextField( async updateContextField(

View File

@ -101,7 +101,7 @@ class StrategyService {
async createStrategy( async createStrategy(
value: IMinimalStrategy, value: IMinimalStrategy,
userName: string, userName: string,
): Promise<void> { ): Promise<IStrategy> {
const strategy = await strategySchema.validateAsync(value); const strategy = await strategySchema.validateAsync(value);
strategy.deprecated = false; strategy.deprecated = false;
await this._validateStrategyName(strategy); await this._validateStrategyName(strategy);
@ -111,6 +111,7 @@ class StrategyService {
createdBy: userName, createdBy: userName,
data: strategy, data: strategy,
}); });
return this.strategyStore.get(strategy.name);
} }
async updateStrategy( async updateStrategy(

View File

@ -52,7 +52,7 @@ export default class TagService {
return data; return data;
} }
async createTag(tag: ITag, userName: string): Promise<void> { async createTag(tag: ITag, userName: string): Promise<ITag> {
const data = await this.validate(tag); const data = await this.validate(tag);
await this.tagStore.createTag(data); await this.tagStore.createTag(data);
await this.eventStore.store({ await this.eventStore.store({
@ -60,6 +60,8 @@ export default class TagService {
createdBy: userName, createdBy: userName,
data, data,
}); });
return data;
} }
async deleteTag(tag: ITag, userName: string): Promise<void> { async deleteTag(tag: ITag, userName: string): Promise<void> {