Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/menu versioning #17

Merged
merged 33 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
29083eb
feat: MenuRevision entity, migration and relationships
Feb 24, 2023
71b9a6c
feat: CreateMenuRevisionInput
Feb 24, 2023
03f5767
feat: Menus service and resolver createRevision
Feb 24, 2023
23421df
feat: Menus service and resolver restoreRevision
Feb 24, 2023
04ec8c6
feat: Menu currentRevision field
Feb 24, 2023
275e704
feat: setting Menu currentRevision
Feb 24, 2023
3f72469
feat: optional setAsCurrent revision
Feb 24, 2023
cc11d39
fix: Menu currentRevision composite foreign key
Feb 24, 2023
e92c28e
feat: MenuItem composite primary key
Feb 24, 2023
f1c7c47
feat: MenuItemSubscriber beforeInsert setId
Feb 25, 2023
b1424a7
feat: EntityNotFoundError and failing when Menu or MenuItem not found
Feb 25, 2023
7a44fc8
refactor: MenuRevision dateCreated -> createdAt
Feb 25, 2023
8507799
feat: Timestamped and VersionedTimestamped abstract objects
Feb 25, 2023
cf3424a
feat: Menu extends VersionedTimestamped
Feb 25, 2023
e1cfb64
feat: MenuItem extends VersionedTimestamped
Feb 25, 2023
db21dd0
feat: docker-compose db service default time zone
Feb 25, 2023
1116d19
feat: excluding Menu timestamps and version from revision snapshot
Feb 25, 2023
3d6fbf6
feat: UTC default timezone
Feb 25, 2023
c409541
fix: MenuRevision menuId not optional
Feb 25, 2023
c28099b
feat: Menu publishedRevision field
Feb 25, 2023
210f278
feat: Menus service and resolver publishRevision
Feb 25, 2023
6a7086c
fix: restoreRevision
Feb 25, 2023
c416052
feat: Edge and Connnection interfaces
Feb 25, 2023
d280708
feat: Menu currentRevisionId and publishedRevisionId
Feb 25, 2023
5759922
refactor: createRevision
Feb 25, 2023
d5472b4
feat: MenuRevisionSnapshot object
Feb 25, 2023
84dcb00
refactor: MenuRevision snapshot using JSONObject scalar because of ca…
Feb 25, 2023
9535f25
fix: Removing meta from items when menu meta is deleted
Mar 1, 2023
71c02af
fix: Menu restoreRevision enable same revision
Mar 1, 2023
2e54f22
fix: Menu findOneByOrFail
Mar 1, 2023
1c38ba3
feat: loading relations on revision restore and publish
Mar 1, 2023
6319b54
refactor: returnig Menu from createRevision
Mar 1, 2023
5e4d016
feat: Menu meta validate defaultValue when meta is required
Mar 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const typeOrmConfig: TypeOrmConfig = (configService: ConfigService) => {
username: configService.get('JAMIE_API_DATABASE_USER'),
password: configService.get('JAMIE_API_DATABASE_PASSWORD'),
database: configService.get('JAMIE_API_DATABASE_NAME'),
timezone: 'Z',
entities: [join(__dirname, '..', 'src', '**', '*.entity{.js,.ts}')],
subscribers: [join(__dirname, '..', 'src', '**', '*.subscriber{.js,.ts}')],
migrationsRun: true,
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ services:
MYSQL_DATABASE: 'api'
MYSQL_USER: 'api'
MYSQL_PASSWORD: 'api'
TZ: '+00:00'
command: --default-time-zone=+00:00

prometheus:
image: bitnami/prometheus
Expand Down
1 change: 1 addition & 0 deletions migrations.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default new DataSource({
username: configService.get('JAMIE_API_DATABASE_USER'),
password: configService.get('JAMIE_API_DATABASE_PASSWORD'),
database: configService.get('JAMIE_API_DATABASE_NAME'),
timezone: 'Z',
entities: [join(__dirname, 'dist', 'src', '**', '*.entity.js')],
migrations: [join(__dirname, 'dist', 'migrations', '*.js')],
namingStrategy: new SnakeNamingStrategy(),
Expand Down
21 changes: 21 additions & 0 deletions migrations/1677257893669-CreateMenuRevisions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateMenuRevisions1677257893669 implements MigrationInterface {
name = 'CreateMenuRevisions1677257893669';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE \`menu_revisions\` (\`id\` int NOT NULL, \`menu_id\` int NOT NULL, \`description\` text NOT NULL, \`snapshot\` text NOT NULL, \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`, \`menu_id\`)) ENGINE=InnoDB`,
);
await queryRunner.query(
`ALTER TABLE \`menu_revisions\` ADD CONSTRAINT \`FK_ae57a5041c46e5dba5810a73030\` FOREIGN KEY (\`menu_id\`) REFERENCES \`menus\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`menu_revisions\` DROP FOREIGN KEY \`FK_ae57a5041c46e5dba5810a73030\``,
);
await queryRunner.query(`DROP TABLE \`menu_revisions\``);
}
}
31 changes: 31 additions & 0 deletions migrations/1677272600645-AddCurrentRevisionToMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddCurrentRevisionToMenu1677272600645
implements MigrationInterface
{
name = 'AddCurrentRevisionToMenu1677272600645';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`menus\` ADD \`current_revision_id\` int NULL`,
);
await queryRunner.query(
`ALTER TABLE \`menus\` ADD \`current_revision_menu_id\` int NULL`,
);
await queryRunner.query(
`ALTER TABLE \`menus\` ADD CONSTRAINT \`FK_27681781e1b6b13225c733fb648\` FOREIGN KEY (\`current_revision_id\`, \`current_revision_menu_id\`) REFERENCES \`menu_revisions\`(\`id\`,\`menu_id\`) ON DELETE CASCADE ON UPDATE CASCADE`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`menus\` DROP FOREIGN KEY \`FK_27681781e1b6b13225c733fb648\``,
);
await queryRunner.query(
`ALTER TABLE \`menus\` DROP COLUMN \`current_revision_menu_id\``,
);
await queryRunner.query(
`ALTER TABLE \`menus\` DROP COLUMN \`current_revision_id\``,
);
}
}
43 changes: 43 additions & 0 deletions migrations/1677279793975-MenuItemCompositePrimaryKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class MenuItemCompositePrimaryKey1677279793975
implements MigrationInterface
{
name = 'MenuItemCompositePrimaryKey1677279793975';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`menu_items\` DROP FOREIGN KEY \`FK_8e20ca40202c116fdafe92cdc4e\``,
);
await queryRunner.query(
`ALTER TABLE \`menu_items\` ADD \`parent_menu_id\` int NULL`,
);
await queryRunner.query(
`ALTER TABLE \`menu_items\` CHANGE \`id\` \`id\` int NOT NULL`,
);
await queryRunner.query(`ALTER TABLE \`menu_items\` DROP PRIMARY KEY`);
await queryRunner.query(
`ALTER TABLE \`menu_items\` ADD PRIMARY KEY (\`id\`, \`menu_id\`)`,
);
await queryRunner.query(
`ALTER TABLE \`menu_items\` ADD CONSTRAINT \`FK_410d05637be571e7fc8973ae364\` FOREIGN KEY (\`parent_id\`, \`parent_menu_id\`) REFERENCES \`menu_items\`(\`id\`,\`menu_id\`) ON DELETE CASCADE ON UPDATE CASCADE`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`menu_items\` DROP FOREIGN KEY \`FK_410d05637be571e7fc8973ae364\``,
);
await queryRunner.query(`ALTER TABLE \`menu_items\` DROP PRIMARY KEY`);
await queryRunner.query(`ALTER TABLE \`menu_items\` DROP COLUMN \`id\``);
await queryRunner.query(
`ALTER TABLE \`menu_items\` ADD \`id\` int NOT NULL AUTO_INCREMENT, ADD PRIMARY KEY (\`id\`)`,
);
await queryRunner.query(
`ALTER TABLE \`menu_items\` DROP COLUMN \`parent_menu_id\``,
);
await queryRunner.query(
`ALTER TABLE \`menu_items\` ADD CONSTRAINT \`FK_8e20ca40202c116fdafe92cdc4e\` FOREIGN KEY (\`parent_id\`) REFERENCES \`menu_items\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`,
);
}
}
29 changes: 29 additions & 0 deletions migrations/1677304809626-AddVersionAndTimestampsToMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddVersionAndTimestampsToMenu1677304809626
implements MigrationInterface
{
name = 'AddVersionAndTimestampsToMenu1677304809626';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`menus\` ADD \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)`,
);
await queryRunner.query(
`ALTER TABLE \`menus\` ADD \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)`,
);
await queryRunner.query(
`ALTER TABLE \`menus\` ADD \`deleted_at\` datetime(6) NULL`,
);
await queryRunner.query(
`ALTER TABLE \`menus\` ADD \`version\` int NOT NULL`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`menus\` DROP COLUMN \`version\``);
await queryRunner.query(`ALTER TABLE \`menus\` DROP COLUMN \`deleted_at\``);
await queryRunner.query(`ALTER TABLE \`menus\` DROP COLUMN \`updated_at\``);
await queryRunner.query(`ALTER TABLE \`menus\` DROP COLUMN \`created_at\``);
}
}
37 changes: 37 additions & 0 deletions migrations/1677305371145-AddVersionAndTimestampsToMenuItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddVersionAndTimestampsToMenuItem1677305371145
implements MigrationInterface
{
name = 'AddVersionAndTimestampsToMenuItem1677305371145';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`menu_items\` ADD \`created_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)`,
);
await queryRunner.query(
`ALTER TABLE \`menu_items\` ADD \`updated_at\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6)`,
);
await queryRunner.query(
`ALTER TABLE \`menu_items\` ADD \`deleted_at\` datetime(6) NULL`,
);
await queryRunner.query(
`ALTER TABLE \`menu_items\` ADD \`version\` int NOT NULL`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`menu_items\` DROP COLUMN \`version\``,
);
await queryRunner.query(
`ALTER TABLE \`menu_items\` DROP COLUMN \`deleted_at\``,
);
await queryRunner.query(
`ALTER TABLE \`menu_items\` DROP COLUMN \`updated_at\``,
);
await queryRunner.query(
`ALTER TABLE \`menu_items\` DROP COLUMN \`created_at\``,
);
}
}
31 changes: 31 additions & 0 deletions migrations/1677320214393-AddPublishedRevisionToMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddPublishedRevisionToMenu1677320214393
implements MigrationInterface
{
name = 'AddPublishedRevisionToMenu1677320214393';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`menus\` ADD \`published_revision_id\` int NULL`,
);
await queryRunner.query(
`ALTER TABLE \`menus\` ADD \`published_revision_menu_id\` int NULL`,
);
await queryRunner.query(
`ALTER TABLE \`menus\` ADD CONSTRAINT \`FK_e4bf608a85adf722bc3015bbb8a\` FOREIGN KEY (\`published_revision_id\`, \`published_revision_menu_id\`) REFERENCES \`menu_revisions\`(\`id\`,\`menu_id\`) ON DELETE CASCADE ON UPDATE CASCADE`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`menus\` DROP FOREIGN KEY \`FK_e4bf608a85adf722bc3015bbb8a\``,
);
await queryRunner.query(
`ALTER TABLE \`menus\` DROP COLUMN \`published_revision_menu_id\``,
);
await queryRunner.query(
`ALTER TABLE \`menus\` DROP COLUMN \`published_revision_id\``,
);
}
}
22 changes: 22 additions & 0 deletions src/common/errors/entity-not-found.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { GraphQLError } from 'graphql';

export class EntityNotFoundError<
T extends string | { name: string },
> extends GraphQLError {
constructor(entity: T, id: number) {
const e =
typeof entity === 'string'
? entity
: entity.constructor?.name !== 'Function'
? entity.constructor.name
: entity.name;
super(`${e} with id ${id} not found`, {
extensions: {
code: 'ENTITY_NOT_FOUND',
entity: e,
id,
},
});
this.name = 'EntityNotFoundError';
}
}
1 change: 1 addition & 0 deletions src/common/errors/field-validation.error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class FieldValidationError extends GraphQLError {
IS_UNIQUE: 'isUnique',
// Menu Meta
META_TYPE_CANNOT_BE_CHANGED: 'metaTypeCannotBeChanged',
META_DEFAULT_VALUE_REQUIRED: 'metaDefaultValueRequired',
// Menu Item Meta
META_REQUIRED: 'metaRequired',
};
Expand Down
13 changes: 7 additions & 6 deletions src/common/helpers/paginate.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ import { PageInfo } from '../schema/objects/page-info.object';
import { PaginationArgs } from '../schema/args/pagination.arg';
import { SelectQueryBuilder, MoreThan, LessThan } from 'typeorm';
import { Direction } from '../schema/enums/direction.enum';
import { Connection } from '../types';

/**
* Based on https://gist.github.com/VojtaSim/6b03466f1964a6c81a3dbf1f8cec8d5c
* Based on https://gist.github.com/tumainimosha/6652deb0aea172f7f2c4b2077c72d16c
*/
export async function paginate<T>(
query: SelectQueryBuilder<T>,
paginationArgs: PaginationArgs,
cursorColumn = 'id',
direction = Direction.ASC,
defaultLimit = 25,
): Promise<any> {
): Promise<Connection<T>> {
const logger = new Logger('Pagination');

// pagination ordering
Expand All @@ -27,7 +28,7 @@ export async function paginate<T>(
const offsetId = Number(
Buffer.from(paginationArgs.after, 'base64').toString('ascii'),
);
logger.verbose(`Paginate AfterID: ${offsetId}`);
logger.verbose(`Paginate After ID: ${offsetId}`);
query.where({ [cursorColumn]: MoreThan(offsetId) });
}

Expand All @@ -41,7 +42,7 @@ export async function paginate<T>(
const offsetId = Number(
Buffer.from(paginationArgs.before, 'base64').toString('ascii'),
);
logger.verbose(`Paginate BeforeID: ${offsetId}`);
logger.verbose(`Paginate Before ID: ${offsetId}`);

const limit = paginationArgs.last ?? defaultLimit;

Expand Down Expand Up @@ -81,8 +82,8 @@ export async function paginate<T>(
.getCount();
}

logger.debug(`CountBefore:${countBefore}`);
logger.debug(`CountAfter:${countAfter}`);
logger.verbose(`CountBefore: ${countBefore}`);
logger.verbose(`CountAfter: ${countAfter}`);

const edges = result.map((value) => {
return {
Expand Down
28 changes: 28 additions & 0 deletions src/common/schema/objects/versioned-timestamped.object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import {
CreateDateColumn,
DeleteDateColumn,
UpdateDateColumn,
VersionColumn,
} from 'typeorm';

@ObjectType({ isAbstract: true })
export abstract class Timestamped {
@Field(() => Date)
@CreateDateColumn()
createdAt: Date;

@Field(() => Date)
@UpdateDateColumn()
updatedAt: Date;

@DeleteDateColumn()
deletedAt?: Date;
}

@ObjectType({ isAbstract: true })
export abstract class VersionedTimestamped extends Timestamped {
@Field(() => Int)
@VersionColumn()
version: number;
}
22 changes: 22 additions & 0 deletions src/common/types/model.type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { MenuItem } from 'src/menu-items/entities/menu-item.entity';
import { MenuMeta } from 'src/menus/objects/menu-meta.object';
import { InputAction } from '../schema/enums/input-action.enum';
import { PageInfo } from '../schema/objects/page-info.object';

export enum MenuMetaType {
TEXT = 'text',
Expand Down Expand Up @@ -26,4 +29,23 @@ export interface IMenuItemMeta {
[index: number]: unknown;
}

export interface MenuRevisionSnapshot {
name: string;
meta?: MenuMeta[];
items?: MenuItem[];
template?: string;
templateFormat?: string;
}

export type WithAction<T> = T & { action?: InputAction };

export interface Edge<T> {
cursor: string;
node: T;
}

export interface Connection<T> {
edges: Edge<T>[];
pageInfo: PageInfo;
totalCount: number;
}
9 changes: 5 additions & 4 deletions src/menu-items/entities/menu-item.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { TemplateFormat } from 'src/common/enums/template-format.enum';
import { VersionedTimestamped } from 'src/common/schema/objects/versioned-timestamped.object';
import { GraphQLJSONObject } from 'src/common/schema/scalars/json.scalar';
import { IMenuItemMeta } from 'src/common/types';
import { Menu } from 'src/menus/entities/menu.entity';
Expand All @@ -9,14 +10,14 @@ import {
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
PrimaryColumn,
} from 'typeorm';

@ObjectType()
@Entity('menu_items')
export class MenuItem {
export class MenuItem extends VersionedTimestamped {
@Field(() => Int)
@PrimaryGeneratedColumn()
@PrimaryColumn()
id: number;

@Field()
Expand Down Expand Up @@ -63,7 +64,7 @@ export class MenuItem {
menu: Menu;

@Field(() => Int)
@Column()
@PrimaryColumn()
menuId?: number;

@Field(() => Boolean, { nullable: false })
Expand Down
Loading