Add routes and fix mappers and migration scripts
This commit is contained in:
parent
58a581fbac
commit
801739ea30
11 changed files with 184 additions and 97 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
import { AbstractDto } from "./AbstractDto.js";
|
import { AbstractDto } from "./AbstractDto.js";
|
||||||
import { RecipeIngredientGroupDto } from "./RecipeIngredientGroupDto.js";
|
import { RecipeIngredientGroupDto } from "./RecipeIngredientGroupDto.js";
|
||||||
import { RecipeInstructionStepDto } from "./RecipeInstructionStepDto.js";
|
import { RecipeInstructionStepDto } from "./RecipeInstructionStepDto.js";
|
||||||
|
import {TagDto} from "./TagDto.js";
|
||||||
/**
|
/**
|
||||||
* DTO describing a recipe
|
* DTO describing a recipe
|
||||||
*/
|
*/
|
||||||
|
|
@ -12,4 +13,5 @@ export class RecipeDto extends AbstractDto {
|
||||||
amountDescription?: string;
|
amountDescription?: string;
|
||||||
instructions!: RecipeInstructionStepDto[];
|
instructions!: RecipeInstructionStepDto[];
|
||||||
ingredientGroups!: RecipeIngredientGroupDto[];
|
ingredientGroups!: RecipeIngredientGroupDto[];
|
||||||
|
tagList?: TagDto[];
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import { CompactRecipeHandler } from "../../handlers/CompactRecipeHandler.js";
|
||||||
import { CompactRecipeDtoEntityMapper } from "../../mappers/CompactRecipeDtoEntityMapper.js";
|
import { CompactRecipeDtoEntityMapper } from "../../mappers/CompactRecipeDtoEntityMapper.js";
|
||||||
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
||||||
|
|
||||||
|
export const compactRecipeBasicRoute = "/compact-recipe"
|
||||||
/**
|
/**
|
||||||
* Handles all recipe related routes
|
* Handles all recipe related routes
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ import { RecipeIngredientDtoEntityMapper } from "../../mappers/RecipeIngredientD
|
||||||
import { RecipeIngredientGroupDtoEntityMapper } from "../../mappers/RecipeIngredientGroupDtoEntityMapper.js";
|
import { RecipeIngredientGroupDtoEntityMapper } from "../../mappers/RecipeIngredientGroupDtoEntityMapper.js";
|
||||||
import { RecipeInstructionStepDtoEntityMapper } from "../../mappers/RecipeInstructionStepDtoEntityMapper.js";
|
import { RecipeInstructionStepDtoEntityMapper } from "../../mappers/RecipeInstructionStepDtoEntityMapper.js";
|
||||||
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
import {HttpStatusCode} from "../apiHelpers/HttpStatusCodes.js";
|
||||||
|
import {TagDtoEntityMapper} from "../../mappers/TagDtoEntityMapper.js";
|
||||||
|
|
||||||
|
export const recipeBasicRoute = "/recipe";
|
||||||
/**
|
/**
|
||||||
* Handles all recipe related routes
|
* Handles all recipe related routes
|
||||||
*/
|
*/
|
||||||
|
|
@ -19,7 +21,8 @@ const recipeRepository = new RecipeRepository();
|
||||||
const recipeIngredientMapper = new RecipeIngredientDtoEntityMapper();
|
const recipeIngredientMapper = new RecipeIngredientDtoEntityMapper();
|
||||||
const recipeIngredientGroupMapper = new RecipeIngredientGroupDtoEntityMapper(recipeIngredientMapper);
|
const recipeIngredientGroupMapper = new RecipeIngredientGroupDtoEntityMapper(recipeIngredientMapper);
|
||||||
const recipeInstructionStepMapper = new RecipeInstructionStepDtoEntityMapper();
|
const recipeInstructionStepMapper = new RecipeInstructionStepDtoEntityMapper();
|
||||||
const recipeMapper = new RecipeDtoEntityMapper(recipeInstructionStepMapper, recipeIngredientGroupMapper);
|
const tagMapper = new TagDtoEntityMapper();
|
||||||
|
const recipeMapper = new RecipeDtoEntityMapper(recipeInstructionStepMapper, recipeIngredientGroupMapper, tagMapper);
|
||||||
const recipeHandler = new RecipeHandler(recipeRepository, recipeMapper);
|
const recipeHandler = new RecipeHandler(recipeRepository, recipeMapper);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,16 @@ import {requireAdmin} from "../../middleware/authorizationMiddleware.js";
|
||||||
* applied upstream (e.g. a global JWT guard). The adminOnly middleware below
|
* applied upstream (e.g. a global JWT guard). The adminOnly middleware below
|
||||||
* adds the role check that restricts DELETE to administrators.
|
* adds the role check that restricts DELETE to administrators.
|
||||||
*/
|
*/
|
||||||
|
export const tagBasicRoute = "/tag";
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const tagHandler = new TagHandler();
|
const tagHandler = new TagHandler();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /tags
|
* GET /tags
|
||||||
* Returns all available tags.
|
* Returns all available tags.
|
||||||
*/
|
*/
|
||||||
router.get("/", async (_req: Request, res: Response, next: NextFunction) => {
|
router.get("/all", async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const tags = await tagHandler.getAll();
|
const tags = await tagHandler.getAll();
|
||||||
res.json(tags);
|
res.json(tags);
|
||||||
|
|
@ -36,6 +37,8 @@ router.get("/", async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
/**
|
/**
|
||||||
* POST /tags/create-or-update
|
* POST /tags/create-or-update
|
||||||
* Creates a new tag or updates an existing one.
|
* Creates a new tag or updates an existing one.
|
||||||
|
* If a tag with the given descriprion already exists in the database, that tag is returned instead of creating
|
||||||
|
* a duplicate.
|
||||||
* Body: TagDto
|
* Body: TagDto
|
||||||
*/
|
*/
|
||||||
router.post(
|
router.post(
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,12 @@ export class RecipeEntity extends AbstractEntity {
|
||||||
@ManyToMany(() => TagEntity, (tag) => tag.recipeList, {
|
@ManyToMany(() => TagEntity, (tag) => tag.recipeList, {
|
||||||
cascade: ["insert", "update"],
|
cascade: ["insert", "update"],
|
||||||
eager: false,
|
eager: false,
|
||||||
|
nullable: true,
|
||||||
})
|
})
|
||||||
@JoinTable({
|
@JoinTable({
|
||||||
name: "recipe_tag",
|
name: "recipe_tag",
|
||||||
joinColumn: { name: "recipe_id", referencedColumnName: "id" },
|
joinColumn: { name: "recipe_id", referencedColumnName: "id" },
|
||||||
inverseJoinColumn: { name: "tag_id", referencedColumnName: "id" },
|
inverseJoinColumn: { name: "tag_id", referencedColumnName: "id" },
|
||||||
})
|
})
|
||||||
tagList!: TagEntity[];
|
tagList?: TagEntity[];
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ import { RecipeEntity } from "./RecipeEntity.js";
|
||||||
*/
|
*/
|
||||||
@Entity({ name: "tag" })
|
@Entity({ name: "tag" })
|
||||||
export class TagEntity extends AbstractEntity {
|
export class TagEntity extends AbstractEntity {
|
||||||
@Column({ type: "varchar", length: 100, unique: true })
|
@Column({ type: "varchar", length: 100, unique: true, nullable: false })
|
||||||
description!: string;
|
description!: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { UserRole } from "../api/enums/UserRole.js";
|
||||||
*/
|
*/
|
||||||
@Entity({ name: "user" })
|
@Entity({ name: "user" })
|
||||||
export class UserEntity extends AbstractEntity {
|
export class UserEntity extends AbstractEntity {
|
||||||
@Column({ nullable: false, name: "user_name" })
|
@Column({ nullable: false, name: "user_name", unique: true })
|
||||||
userName!: string;
|
userName!: string;
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
|
|
|
||||||
10
src/index.ts
10
src/index.ts
|
|
@ -9,8 +9,9 @@ import { logger } from "./utils/logger.js";
|
||||||
import { HttpStatusCode } from "./api/apiHelpers/HttpStatusCodes.js";
|
import { HttpStatusCode } from "./api/apiHelpers/HttpStatusCodes.js";
|
||||||
import authRoutes, { authBasicRoute } from "./api/endpoints/AuthRestResource.js";
|
import authRoutes, { authBasicRoute } from "./api/endpoints/AuthRestResource.js";
|
||||||
import userRoutes, { userBasicRoute } from "./api/endpoints/UserRestResource.js";
|
import userRoutes, { userBasicRoute } from "./api/endpoints/UserRestResource.js";
|
||||||
import compactRecipeRoutes from "./api/endpoints/CompactRecipeRestResource.js";
|
import compactRecipeRoutes, {compactRecipeBasicRoute} from "./api/endpoints/CompactRecipeRestResource.js";
|
||||||
import recipeRoutes from "./api/endpoints/RecipeRestResource.js";
|
import recipeRoutes, {recipeBasicRoute} from "./api/endpoints/RecipeRestResource.js";
|
||||||
|
import tagRoutes, {tagBasicRoute} from "./api/endpoints/TagRestResource.js";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
|
@ -46,8 +47,9 @@ async function startServer() {
|
||||||
// Setup routes
|
// Setup routes
|
||||||
app.use(authBasicRoute, authRoutes);
|
app.use(authBasicRoute, authRoutes);
|
||||||
app.use(userBasicRoute, userRoutes);
|
app.use(userBasicRoute, userRoutes);
|
||||||
app.use("/recipe", recipeRoutes);
|
app.use(recipeBasicRoute, recipeRoutes);
|
||||||
app.use("/compact-recipe", compactRecipeRoutes);
|
app.use(compactRecipeBasicRoute, compactRecipeRoutes);
|
||||||
|
app.use(tagBasicRoute,tagRoutes)
|
||||||
|
|
||||||
// Catch-all for unknown routes
|
// Catch-all for unknown routes
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,14 @@ import { RecipeEntity } from "../entities/RecipeEntity.js";
|
||||||
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
import { AbstractDtoEntityMapper } from "./AbstractDtoEntityMapper.js";
|
||||||
import { RecipeIngredientGroupDtoEntityMapper } from "./RecipeIngredientGroupDtoEntityMapper.js";
|
import { RecipeIngredientGroupDtoEntityMapper } from "./RecipeIngredientGroupDtoEntityMapper.js";
|
||||||
import { RecipeInstructionStepDtoEntityMapper } from "./RecipeInstructionStepDtoEntityMapper.js";
|
import { RecipeInstructionStepDtoEntityMapper } from "./RecipeInstructionStepDtoEntityMapper.js";
|
||||||
|
import { TagDtoEntityMapper } from "./TagDtoEntityMapper.js";
|
||||||
|
|
||||||
export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity,RecipeDto>{
|
export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity, RecipeDto> {
|
||||||
constructor(
|
constructor(
|
||||||
private instructionStepMapper : RecipeInstructionStepDtoEntityMapper,
|
private instructionStepMapper: RecipeInstructionStepDtoEntityMapper,
|
||||||
private ingredientGroupMapper : RecipeIngredientGroupDtoEntityMapper
|
private ingredientGroupMapper: RecipeIngredientGroupDtoEntityMapper,
|
||||||
){
|
private tagMapper: TagDtoEntityMapper
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,83 +21,110 @@ export class RecipeDtoEntityMapper extends AbstractDtoEntityMapper<RecipeEntity,
|
||||||
this.mapBaseEntityToDto(entity, dto);
|
this.mapBaseEntityToDto(entity, dto);
|
||||||
|
|
||||||
dto.title = entity.title;
|
dto.title = entity.title;
|
||||||
dto.amount = entity.amount
|
dto.amount = entity.amount;
|
||||||
dto.amountDescription = entity.amountDescription;
|
dto.amountDescription = entity.amountDescription;
|
||||||
|
|
||||||
// map instructions
|
// map instructions
|
||||||
dto.instructions = entity.instructionSteps.map((stepEntity) => {
|
dto.instructions = entity.instructionSteps.map((stepEntity) => {
|
||||||
const instructionStep : RecipeInstructionStepDto = this.instructionStepMapper.toDto(stepEntity);
|
const instructionStep: RecipeInstructionStepDto = this.instructionStepMapper.toDto(stepEntity);
|
||||||
instructionStep.recipeId = entity.id;
|
instructionStep.recipeId = entity.id;
|
||||||
return instructionStep;
|
return instructionStep;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// map ingredient groups
|
// map ingredient groups
|
||||||
dto.ingredientGroups = entity.ingredientGroups.map((groupEntity) => {
|
dto.ingredientGroups = entity.ingredientGroups.map((groupEntity) => {
|
||||||
const ingredientGroup :RecipeIngredientGroupDto = this.ingredientGroupMapper.toDto(groupEntity);
|
const ingredientGroup: RecipeIngredientGroupDto = this.ingredientGroupMapper.toDto(groupEntity);
|
||||||
ingredientGroup.recipeId = entity.id;
|
ingredientGroup.recipeId = entity.id;
|
||||||
return ingredientGroup;
|
return ingredientGroup;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// map tags
|
||||||
|
dto.tagList = (entity.tagList ?? []).map((tagEntity) =>
|
||||||
|
this.tagMapper.toDto(tagEntity)
|
||||||
|
);
|
||||||
|
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
toEntity(dto: RecipeDto): RecipeEntity {
|
toEntity(dto: RecipeDto): RecipeEntity {
|
||||||
const entity = new RecipeEntity();
|
const entity = new RecipeEntity();
|
||||||
this.mapBaseDtoToEntity(dto, entity);
|
this.mapBaseDtoToEntity(dto, entity);
|
||||||
|
|
||||||
entity.title = dto.title;
|
entity.title = dto.title;
|
||||||
entity.amount = dto.amount
|
entity.amount = dto.amount;
|
||||||
entity.amountDescription = dto.amountDescription;
|
entity.amountDescription = dto.amountDescription;
|
||||||
|
|
||||||
// map instructions
|
// map instructions
|
||||||
entity.instructionSteps = dto.instructions.map((stepDto) => {
|
entity.instructionSteps = dto.instructions.map((stepDto) => {
|
||||||
const stepEntity = this.instructionStepMapper.toEntity(stepDto);
|
const stepEntity = this.instructionStepMapper.toEntity(stepDto);
|
||||||
|
|
||||||
// Set the relation if the entity already exists in DB
|
// Set the relation if the entity already exists in DB
|
||||||
if(entity.hasValidId()){
|
if (entity.hasValidId()) {
|
||||||
stepEntity.recipe = entity;
|
stepEntity.recipe = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a new step (no id from client), let DB generate a new UUID
|
||||||
|
if (!stepDto.id) {
|
||||||
|
delete (stepEntity as any).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stepEntity;
|
||||||
|
});
|
||||||
|
|
||||||
|
// map ingredient groups
|
||||||
|
entity.ingredientGroups = dto.ingredientGroups.map((groupDto) => {
|
||||||
|
const groupEntity = this.ingredientGroupMapper.toEntity(groupDto);
|
||||||
|
|
||||||
|
// Set the relation if the entity already exists in DB
|
||||||
|
if (entity.hasValidId()) {
|
||||||
|
groupEntity.recipe = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a new group (no id from client), let DB generate a new UUID
|
||||||
|
if (!groupDto.id) {
|
||||||
|
delete (groupEntity as any).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupEntity;
|
||||||
|
});
|
||||||
|
|
||||||
|
// map tags
|
||||||
|
entity.tagList = (dto.tagList ?? []).map((tagDto) =>
|
||||||
|
this.tagMapper.toEntity(tagDto)
|
||||||
|
);
|
||||||
|
|
||||||
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it's a new step (no id from client), let DB generate a new UUID
|
mergeDtoIntoEntity(dto: RecipeDto, entity: RecipeEntity): RecipeEntity {
|
||||||
if (!stepDto.id) {
|
entity.title = dto.title;
|
||||||
delete (stepEntity as any).id;
|
entity.amount = dto.amount;
|
||||||
|
entity.amountDescription = dto.amountDescription;
|
||||||
|
|
||||||
|
// --- Instruction Steps ---
|
||||||
|
entity.instructionSteps = this.instructionStepMapper.mergeDtoListIntoEntityList(
|
||||||
|
dto.instructions,
|
||||||
|
entity.instructionSteps
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Ingredient Groups ---
|
||||||
|
entity.ingredientGroups = this.ingredientGroupMapper.mergeDtoListIntoEntityList(
|
||||||
|
dto.ingredientGroups,
|
||||||
|
entity.ingredientGroups
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Tags ---
|
||||||
|
// Tags are looked up by ID and replaced wholesale: the recipe holds
|
||||||
|
// references to existing tag entities, so we map each DTO to its entity
|
||||||
|
// form and let TypeORM sync the join table on save.
|
||||||
|
entity.tagList = (dto.tagList ?? []).map((tagDto) =>
|
||||||
|
this.tagMapper.toEntity(tagDto)
|
||||||
|
);
|
||||||
|
|
||||||
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
return stepEntity;
|
createNewEntity(): RecipeEntity {
|
||||||
});
|
return new RecipeEntity();
|
||||||
|
|
||||||
// map ingredient groups
|
|
||||||
entity.ingredientGroups = dto.ingredientGroups.map((groupDto) => {
|
|
||||||
const groupEntity = this.ingredientGroupMapper.toEntity(groupDto);
|
|
||||||
// Set the relation if the entity already exists in DB
|
|
||||||
if(entity.hasValidId()){
|
|
||||||
groupEntity.recipe = entity;
|
|
||||||
}
|
}
|
||||||
// If it's a new group (no id from client), let DB generate a new UUID
|
|
||||||
if (!groupDto.id) {
|
|
||||||
delete (groupEntity as any).id;
|
|
||||||
}
|
|
||||||
return groupEntity;
|
|
||||||
});
|
|
||||||
|
|
||||||
return entity;
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeDtoIntoEntity(dto: RecipeDto, entity: RecipeEntity): RecipeEntity {
|
|
||||||
entity.title = dto.title;
|
|
||||||
entity.amount = dto.amount;
|
|
||||||
entity.amountDescription = dto.amountDescription;
|
|
||||||
|
|
||||||
// --- Instruction Steps ---
|
|
||||||
entity.instructionSteps = this.instructionStepMapper.mergeDtoListIntoEntityList(dto.instructions, entity.instructionSteps);
|
|
||||||
|
|
||||||
// --- Ingredient Groups ---
|
|
||||||
entity.ingredientGroups = this.ingredientGroupMapper.mergeDtoListIntoEntityList(dto.ingredientGroups, entity.ingredientGroups);
|
|
||||||
return entity
|
|
||||||
}
|
|
||||||
|
|
||||||
createNewEntity(): RecipeEntity {
|
|
||||||
return new RecipeEntity();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,44 +1,78 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migration to convert role column from varchar to enum
|
* Converts the `role` column on the `user` table from varchar to a proper
|
||||||
|
* PostgreSQL enum type.
|
||||||
*
|
*
|
||||||
* File name should be: TIMESTAMP-ConvertRoleToEnum.ts
|
* Existing rows are safe: the varchar values 'user' and 'admin' inserted by
|
||||||
* Example: 1701234567890-ConvertRoleToEnum.ts
|
* CreateUserTable1661234567890 match the enum members exactly, so the USING
|
||||||
|
* cast succeeds without any data transformation.
|
||||||
*
|
*
|
||||||
* Place in: src/migrations/
|
* down() reverts cleanly by casting back to text and dropping the enum type.
|
||||||
*/
|
*/
|
||||||
export class ConvertRoleToEnum1701234567890 implements MigrationInterface {
|
export class ConvertRoleToEnum1701234567890 implements MigrationInterface {
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// Create the enum type if it doesn't already exist
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN
|
||||||
CREATE TYPE user_role_enum AS ENUM ('user', 'admin');
|
CREATE TYPE user_role_enum AS ENUM ('user', 'admin');
|
||||||
EXCEPTION
|
EXCEPTION
|
||||||
WHEN duplicate_object THEN null;
|
WHEN duplicate_object THEN null;
|
||||||
END $$;
|
END $$;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Ensure any existing role values that differ only by whitespace or
|
||||||
|
// casing are normalised before the cast
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
ALTER TABLE "user"
|
UPDATE "user"
|
||||||
ALTER COLUMN "role" TYPE user_role_enum
|
SET "role" = LOWER(TRIM("role"))
|
||||||
USING "role"::user_role_enum;
|
WHERE "role" IS NOT NULL;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Drop the varchar default before retyping — PostgreSQL cannot cast it
|
||||||
|
// automatically and will reject the ALTER COLUMN otherwise
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
ALTER TABLE "user"
|
ALTER TABLE "user"
|
||||||
ALTER COLUMN "role" SET DEFAULT 'user';
|
ALTER COLUMN "role" DROP DEFAULT;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Convert the column — existing 'user' and 'admin' values cast directly
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "user"
|
||||||
|
ALTER COLUMN "role" TYPE user_role_enum
|
||||||
|
USING "role"::user_role_enum;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Re-apply the default using the enum literal
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "user"
|
||||||
|
ALTER COLUMN "role" SET DEFAULT 'user'::user_role_enum;
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// Drop the enum default first so the column can be retyped
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
ALTER TABLE "user"
|
ALTER TABLE "user"
|
||||||
ALTER COLUMN "role" TYPE varchar
|
ALTER COLUMN "role" DROP DEFAULT;
|
||||||
USING "role"::text;
|
`);
|
||||||
`);
|
|
||||||
|
|
||||||
|
// Cast enum values back to plain text
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
DROP TYPE IF EXISTS user_role_enum;
|
ALTER TABLE "user"
|
||||||
`);
|
ALTER COLUMN "role" TYPE varchar
|
||||||
|
USING "role"::text;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Restore the original varchar default
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE "user"
|
||||||
|
ALTER COLUMN "role" SET DEFAULT 'user';
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Drop the enum type — only safe once no column references it
|
||||||
|
await queryRunner.query(`
|
||||||
|
DROP TYPE IF EXISTS user_role_enum;
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {MigrationInterface, QueryRunner, Table} from "typeorm";
|
import {MigrationInterface, QueryRunner, Table, TableUnique} from "typeorm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the `tag` table.
|
* Creates the `tag` table.
|
||||||
|
|
@ -15,6 +15,7 @@ export class CreateTagTable1771658108802 implements MigrationInterface {
|
||||||
type: "uuid",
|
type: "uuid",
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
generationStrategy: "uuid",
|
generationStrategy: "uuid",
|
||||||
|
isGenerated: true,
|
||||||
default: "uuid_generate_v4()",
|
default: "uuid_generate_v4()",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -38,9 +39,20 @@ export class CreateTagTable1771658108802 implements MigrationInterface {
|
||||||
}),
|
}),
|
||||||
true // ifNotExists
|
true // ifNotExists
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add a unique constraint on desciption
|
||||||
|
await queryRunner.createUniqueConstraint(
|
||||||
|
"tag",
|
||||||
|
new TableUnique({
|
||||||
|
columnNames: ["description"],
|
||||||
|
name: "UQ_tag_description",
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// Drop the unique constraint first
|
||||||
|
await queryRunner.dropUniqueConstraint("tag", "UQ_tag_description");
|
||||||
await queryRunner.dropTable("tag", true);
|
await queryRunner.dropTable("tag", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue