diff --git a/src/lib/db/context-field-store.ts b/src/lib/db/context-field-store.ts index 46be952224..904a4f5ec6 100644 --- a/src/lib/db/context-field-store.ts +++ b/src/lib/db/context-field-store.ts @@ -4,6 +4,7 @@ import { IContextField, IContextFieldDto, IContextFieldStore, + ILegalValue, } from '../types/stores/context-field-store'; const COLUMNS = [ @@ -16,7 +17,16 @@ const COLUMNS = [ ]; 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, description: row.description, stickiness: row.stickiness, @@ -88,15 +98,17 @@ class ContextFieldStore implements IContextFieldStore { return present; } + // TODO: write tests for the changes you made here? async create(contextField: IContextFieldDto): Promise { - const row = await this.db(TABLE) + const [row] = await this.db(TABLE) .insert(this.fieldToRow(contextField)) .returning('*'); + return mapRow(row); } async update(data: IContextFieldDto): Promise { - const row = await this.db(TABLE) + const [row] = await this.db(TABLE) .where({ name: data.name }) .update(this.fieldToRow(data)) .returning('*'); diff --git a/src/lib/openapi/util/create-response-schema.ts b/src/lib/openapi/util/create-response-schema.ts index f8886fbacb..d2d58f6341 100644 --- a/src/lib/openapi/util/create-response-schema.ts +++ b/src/lib/openapi/util/create-response-schema.ts @@ -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}`, + }, + }, + }, + }; +}; diff --git a/src/lib/openapi/validate.ts b/src/lib/openapi/validate.ts index 7d2e59ba78..720e1a8f8c 100644 --- a/src/lib/openapi/validate.ts +++ b/src/lib/openapi/validate.ts @@ -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 // let's add it back in! diff --git a/src/lib/routes/admin-api/api-token.ts b/src/lib/routes/admin-api/api-token.ts index aeba07c675..92b62aa2f6 100644 --- a/src/lib/routes/admin-api/api-token.ts +++ b/src/lib/routes/admin-api/api-token.ts @@ -19,7 +19,10 @@ import { createApiToken } from '../../schema/api-token-schema'; import { OpenApiService } from '../../services/openapi-service'; import { IUnleashServices } from '../../types'; 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 { apiTokensSchema, ApiTokensSchema, @@ -96,7 +99,7 @@ export class ApiTokenController extends Controller { operationId: 'createApiToken', requestBody: createRequestSchema('createApiTokenSchema'), responses: { - 201: createResponseSchema('apiTokenSchema'), + 201: resourceCreatedResponseSchema('apiTokenSchema'), }, }), ], diff --git a/src/lib/routes/admin-api/context.ts b/src/lib/routes/admin-api/context.ts index 139fe4945a..710aaf75da 100644 --- a/src/lib/routes/admin-api/context.ts +++ b/src/lib/routes/admin-api/context.ts @@ -24,7 +24,10 @@ import { import { ContextFieldsSchema } from '../../openapi/spec/context-fields-schema'; import { UpsertContextFieldSchema } from '../../openapi/spec/upsert-context-field-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 NotFoundError from '../../error/notfound-error'; import { NameSchema } from '../../openapi/spec/name-schema'; @@ -98,7 +101,9 @@ export class ContextController extends Controller { 'upsertContextFieldSchema', ), responses: { - 201: emptyResponse, + 201: resourceCreatedResponseSchema( + 'contextFieldSchema', + ), }, }), ], @@ -189,13 +194,19 @@ export class ContextController extends Controller { async createContextField( req: IAuthRequest, - res: Response, + res: Response, ): Promise { const value = req.body; const userName = extractUsername(req); - await this.contextService.createContextField(value, userName); - res.status(201).end(); + const result = await this.contextService.createContextField( + 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( diff --git a/src/lib/routes/admin-api/feature.ts b/src/lib/routes/admin-api/feature.ts index 9f98537cc8..d88f98d80d 100644 --- a/src/lib/routes/admin-api/feature.ts +++ b/src/lib/routes/admin-api/feature.ts @@ -25,7 +25,10 @@ import { TagsSchema } from '../../openapi/spec/tags-schema'; import { serializeDates } from '../../types/serialize-dates'; import { OpenApiService } from '../../services/openapi-service'; 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'; const version = 1; @@ -123,7 +126,9 @@ class FeatureController extends Controller { tags: ['Features'], operationId: 'addTag', requestBody: createRequestSchema('tagSchema'), - responses: { 201: createResponseSchema('tagSchema') }, + responses: { + 201: resourceCreatedResponseSchema('tagSchema'), + }, }), ], }); @@ -221,7 +226,7 @@ class FeatureController extends Controller { req.body, userName, ); - res.status(201).json(tag); + res.status(201).header('location', `${featureName}/tags`).json(tag); } // TODO diff --git a/src/lib/routes/admin-api/strategy.ts b/src/lib/routes/admin-api/strategy.ts index d29d6005d0..07e08c2629 100644 --- a/src/lib/routes/admin-api/strategy.ts +++ b/src/lib/routes/admin-api/strategy.ts @@ -15,7 +15,10 @@ import { IAuthRequest } from '../unleash-types'; import { OpenApiService } from '../../services/openapi-service'; import { emptyResponse } from '../../openapi/util/standard-responses'; 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 { strategySchema, StrategySchema, @@ -102,7 +105,9 @@ class StrategyController extends Controller { tags: ['Strategies'], operationId: 'createStrategy', requestBody: createRequestSchema('upsertStrategySchema'), - responses: { 201: emptyResponse }, + responses: { + 201: resourceCreatedResponseSchema('strategySchema'), + }, }), ], }); @@ -193,12 +198,18 @@ class StrategyController extends Controller { async createStrategy( req: IAuthRequest, - res: Response, + res: Response, ): Promise { const userName = extractUsername(req); - await this.strategyService.createStrategy(req.body, userName); - res.status(201).end(); + const strategy = await this.strategyService.createStrategy( + req.body, + userName, + ); + res.header('location', `strategies/${strategy.name}`) + .status(201) + .json(strategy) + .end(); } async updateStrategy( diff --git a/src/lib/routes/admin-api/tag-type.ts b/src/lib/routes/admin-api/tag-type.ts index 3880f11eb7..1dd584e47c 100644 --- a/src/lib/routes/admin-api/tag-type.ts +++ b/src/lib/routes/admin-api/tag-type.ts @@ -13,7 +13,10 @@ import TagTypeService from '../../services/tag-type-service'; import { Logger } from '../../logger'; import { IAuthRequest } from '../unleash-types'; 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 { ValidateTagTypeSchema } from '../../openapi/spec/validate-tag-type-schema'; import { @@ -66,7 +69,9 @@ class TagTypeController extends Controller { openApiService.validPath({ tags: ['Tags'], operationId: 'createTagType', - responses: { 201: createResponseSchema('tagTypeSchema') }, + responses: { + 201: resourceCreatedResponseSchema('tagTypeSchema'), + }, requestBody: createRequestSchema('tagTypeSchema'), }), ], diff --git a/src/lib/routes/admin-api/tag.ts b/src/lib/routes/admin-api/tag.ts index e859d0df90..f15ffcc6e7 100644 --- a/src/lib/routes/admin-api/tag.ts +++ b/src/lib/routes/admin-api/tag.ts @@ -10,7 +10,10 @@ import { NONE, UPDATE_FEATURE } from '../../types/permissions'; import { extractUsername } from '../../util/extract-user'; import { IAuthRequest } from '../unleash-types'; 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 { TagSchema } from '../../openapi/spec/tag-schema'; import { OpenApiService } from '../../services/openapi-service'; @@ -64,7 +67,9 @@ class TagController extends Controller { tags: ['Tags'], operationId: 'createTag', responses: { - 201: emptyResponse, + 201: resourceCreatedResponseSchema( + 'tagWithVersionSchema', + ), }, requestBody: createRequestSchema('tagSchema'), }), @@ -157,11 +162,14 @@ class TagController extends Controller { async createTag( req: IAuthRequest, - res: Response, + res: Response, ): Promise { const userName = extractUsername(req); - await this.tagService.createTag(req.body, userName); - res.status(201).end(); + const tag = await this.tagService.createTag(req.body, userName); + res.status(201) + .header('location', `tags/${tag.type}/${tag.value}`) + .json({ version, tag }) + .end(); } async deleteTag( diff --git a/src/lib/services/context-service.ts b/src/lib/services/context-service.ts index 305310453e..584613c54c 100644 --- a/src/lib/services/context-service.ts +++ b/src/lib/services/context-service.ts @@ -55,18 +55,20 @@ class ContextService { async createContextField( value: IContextFieldDto, userName: string, - ): Promise { + ): Promise { // validations await this.validateUniqueName(value); const contextField = await contextSchema.validateAsync(value); // creations - await this.contextFieldStore.create(value); + const createdField = await this.contextFieldStore.create(value); await this.eventStore.store({ type: CONTEXT_FIELD_CREATED, createdBy: userName, data: contextField, }); + + return createdField; } async updateContextField( diff --git a/src/lib/services/strategy-service.ts b/src/lib/services/strategy-service.ts index 26343aff42..a6c4dcd343 100644 --- a/src/lib/services/strategy-service.ts +++ b/src/lib/services/strategy-service.ts @@ -101,7 +101,7 @@ class StrategyService { async createStrategy( value: IMinimalStrategy, userName: string, - ): Promise { + ): Promise { const strategy = await strategySchema.validateAsync(value); strategy.deprecated = false; await this._validateStrategyName(strategy); @@ -111,6 +111,7 @@ class StrategyService { createdBy: userName, data: strategy, }); + return this.strategyStore.get(strategy.name); } async updateStrategy( diff --git a/src/lib/services/tag-service.ts b/src/lib/services/tag-service.ts index 8bed11b3f8..6f7c62050e 100644 --- a/src/lib/services/tag-service.ts +++ b/src/lib/services/tag-service.ts @@ -52,7 +52,7 @@ export default class TagService { return data; } - async createTag(tag: ITag, userName: string): Promise { + async createTag(tag: ITag, userName: string): Promise { const data = await this.validate(tag); await this.tagStore.createTag(data); await this.eventStore.store({ @@ -60,6 +60,8 @@ export default class TagService { createdBy: userName, data, }); + + return data; } async deleteTag(tag: ITag, userName: string): Promise {