From ec9e79c08622b065990eb6f5e024aa1f75bab463 Mon Sep 17 00:00:00 2001 From: GeekaN2 Date: Sun, 22 Oct 2023 22:29:28 +0300 Subject: [PATCH 1/7] feat: add default tool to user editor tools --- src/domain/service/editorTools.ts | 9 ++++++- src/presentation/http/router/user.ts | 11 +++++++-- src/repository/editorTools.repository.ts | 7 ++++++ .../postgres/orm/sequelize/editorTools.ts | 24 +++++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/domain/service/editorTools.ts b/src/domain/service/editorTools.ts index a25e51d2..c956e0f8 100644 --- a/src/domain/service/editorTools.ts +++ b/src/domain/service/editorTools.ts @@ -32,10 +32,17 @@ export default class EditorToolsService { * * @param editorToolIds - tool ids */ - public async getToolsByIds(editorToolIds: EditorTool['id'][] ): Promise { + public async getToolsByIds(editorToolIds: EditorTool['id'][]): Promise { return await this.repository.getToolsByIds(editorToolIds); } + /** + * Get all default tools + */ + public async getDefaultEditorTools(): Promise { + return await this.repository.getDefaultEditorTools(); + } + /** * Adding custom editor tool * diff --git a/src/presentation/http/router/user.ts b/src/presentation/http/router/user.ts index 2b1e120e..30199607 100644 --- a/src/presentation/http/router/user.ts +++ b/src/presentation/http/router/user.ts @@ -95,10 +95,17 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don const userExtensions = await userService.getUserExtensions(userId); const userEditorToolIds = userExtensions?.editorTools?.map(tools => tools.id) ?? []; - const editorTools = await editorToolsService.getToolsByIds(userEditorToolIds) ?? []; + + const defaultEditorTools = await editorToolsService.getDefaultEditorTools(); + const uniqueDefaultEditorTools = defaultEditorTools.filter(({ id }) => !userEditorToolIds.includes(id)); + const userEditorTools = await editorToolsService.getToolsByIds(userEditorToolIds) ?? []; + + // Combine user tools and default tools + // TODO: load tools in notes service + const mergedTools = [...userEditorTools, ...uniqueDefaultEditorTools]; return reply.send({ - data: editorTools, + data: mergedTools, }); }); diff --git a/src/repository/editorTools.repository.ts b/src/repository/editorTools.repository.ts index 9a20d359..bbe15e68 100644 --- a/src/repository/editorTools.repository.ts +++ b/src/repository/editorTools.repository.ts @@ -34,6 +34,13 @@ export default class EditorToolsRepository { return tools; } + /** + * Get all default tools + */ + public async getDefaultEditorTools(): Promise { + return await this.storage.getDefaultEditorTools(); + } + /** * Get all editor tools */ diff --git a/src/repository/storage/postgres/orm/sequelize/editorTools.ts b/src/repository/storage/postgres/orm/sequelize/editorTools.ts index f20d3c17..3daad4fa 100644 --- a/src/repository/storage/postgres/orm/sequelize/editorTools.ts +++ b/src/repository/storage/postgres/orm/sequelize/editorTools.ts @@ -31,6 +31,11 @@ export class EditorToolModel extends Model, Inf * Editor tool sources */ public declare source: EditorTool['source']; + + /** + * Applies to user editor tools by default + */ + public declare isDefault: EditorTool['isDefault']; } /** @@ -85,6 +90,10 @@ export default class UserSequelizeStorage { type: DataTypes.JSON, allowNull: false, }, + isDefault: { + type: DataTypes.BOOLEAN, + allowNull: true, + }, }, { tableName: this.tableName, sequelize: this.database, @@ -101,6 +110,7 @@ export default class UserSequelizeStorage { title, exportName, source, + isDefault, }: EditorTool): Promise { const editorTool = await this.model.create({ id, @@ -108,6 +118,7 @@ export default class UserSequelizeStorage { title, exportName, source, + isDefault, }); return editorTool; @@ -130,6 +141,19 @@ export default class UserSequelizeStorage { return editorTools; } + /** + * Get all default tools + */ + public async getDefaultEditorTools(): Promise { + const editorTools = await this.model.findAll({ + where: { + isDefault: true, + }, + }); + + return editorTools; + } + /** * Get all available editor tools */ From 2746ae908a63f88fc7f4cf77c27abdfa9a2298de Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 23 Oct 2023 00:10:10 +0300 Subject: [PATCH 2/7] refactoring: get rid of underscore from code --- ...ers@add-editor-tools-remove-extensions.sql | 22 ++++++++ ...r-tools@rename-camelcase-to-underscore.sql | 24 +++++++++ src/domain/entities/user.ts | 7 ++- src/domain/entities/userExtensions.ts | 19 ------- src/domain/service/user.ts | 19 +++---- src/presentation/http/router/user.ts | 12 ++--- src/presentation/http/schema/User.ts | 7 +++ .../postgres/orm/sequelize/editorTools.ts | 18 ++----- .../storage/postgres/orm/sequelize/index.ts | 6 +++ .../storage/postgres/orm/sequelize/note.ts | 25 ++------- .../postgres/orm/sequelize/noteSettings.ts | 1 - .../storage/postgres/orm/sequelize/user.ts | 53 +++++++------------ .../postgres/orm/sequelize/userSession.ts | 46 ++++++---------- src/repository/user.repository.ts | 20 +++---- 14 files changed, 131 insertions(+), 148 deletions(-) create mode 100644 migrations/tenant/0008-users@add-editor-tools-remove-extensions.sql create mode 100644 migrations/tenant/0009-editor-tools@rename-camelcase-to-underscore.sql delete mode 100644 src/domain/entities/userExtensions.ts diff --git a/migrations/tenant/0008-users@add-editor-tools-remove-extensions.sql b/migrations/tenant/0008-users@add-editor-tools-remove-extensions.sql new file mode 100644 index 00000000..d7168f02 --- /dev/null +++ b/migrations/tenant/0008-users@add-editor-tools-remove-extensions.sql @@ -0,0 +1,22 @@ + +-- Adds "editor_tools" column at "users" if not exists +DO $$ +BEGIN + IF NOT EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='users' and column_name='editor_tools') + THEN + ALTER TABLE "public"."users" ADD COLUMN "editor_tools" jsonb; + END IF; +END $$; + +-- Removes "extensions" column at "users" if exists +DO $$ +BEGIN + IF EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='users' and column_name='extensions') + THEN + ALTER TABLE "public"."users" DROP COLUMN "extensions"; + END IF; +END $$; diff --git a/migrations/tenant/0009-editor-tools@rename-camelcase-to-underscore.sql b/migrations/tenant/0009-editor-tools@rename-camelcase-to-underscore.sql new file mode 100644 index 00000000..7ee601fd --- /dev/null +++ b/migrations/tenant/0009-editor-tools@rename-camelcase-to-underscore.sql @@ -0,0 +1,24 @@ +/* + Rename columns in "editor_tools" table: +*/ +-- Rename column "isDefault" to "is_default" if exists +DO $$ +BEGIN + IF EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='editor_tools' and column_name='isDefault') + THEN + ALTER TABLE "public"."editor_tools" RENAME COLUMN "isDefault" TO "is_default"; + END IF; +END $$; + +-- Rename column "exportName" to "export_name" if exists +DO $$ +BEGIN + IF EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='editor_tools' and column_name='exportName') + THEN + ALTER TABLE "public"."editor_tools" RENAME COLUMN "exportName" TO "export_name"; + END IF; +END $$; diff --git a/src/domain/entities/user.ts b/src/domain/entities/user.ts index 08a7dc0c..32957ede 100644 --- a/src/domain/entities/user.ts +++ b/src/domain/entities/user.ts @@ -1,4 +1,4 @@ -import type UserExtensions from '@domain/entities/userExtensions.js'; +import type EditorTool from './editorTools'; /** * User entity @@ -30,8 +30,7 @@ export default interface User { photo?: string; /** - * Custom plugins from the marketplace that improve - * editor or notes environment + * Custom plugins ids from the marketplace that improve editor or notes environment */ - extensions?: UserExtensions; + editorTools?: EditorTool['id'][]; } diff --git a/src/domain/entities/userExtensions.ts b/src/domain/entities/userExtensions.ts deleted file mode 100644 index 4ee4acc4..00000000 --- a/src/domain/entities/userExtensions.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type EditorTool from '@domain/entities/editorTools.js'; - -/** - * Tools that user uses in the editor while changing notes - */ -export interface UserEditorTool { - /** - * Unique tool identifier - */ - id: EditorTool['id']; -} - -/** - * Custom user extensions and plugin that expand the capabilities - * of the editor - */ -export default interface UserExtensions { - editorTools?: UserEditorTool[]; -} diff --git a/src/domain/service/user.ts b/src/domain/service/user.ts index f734bdb3..c58d3b93 100644 --- a/src/domain/service/user.ts +++ b/src/domain/service/user.ts @@ -47,15 +47,18 @@ export default class UserService { } /** - * Get user extensions that contains only editoTools for now - * TODO: Simplify extenisons + * Get user editor tools ids * * @param userId - user unique identifier */ - public async getUserExtensions(userId: User['id']): Promise { + public async getUserEditorTools(userId: User['id']): Promise { const user = await this.getUserById(userId); - return user?.extensions ?? {}; + if (user === null) { + throw new Error('User not found'); + } + + return user.editorTools ?? []; } /** @@ -65,16 +68,14 @@ export default class UserService { */ public async addUserEditorTool({ userId, - editorToolId, + toolId, }: { userId: User['id'], - editorToolId: EditorTool['id'], + toolId: EditorTool['id'], }): Promise { return await this.repository.addUserEditorTool({ userId, - tool: { - id: editorToolId, - }, + toolId, }); } } diff --git a/src/presentation/http/router/user.ts b/src/presentation/http/router/user.ts index 30199607..f2aa2de8 100644 --- a/src/presentation/http/router/user.ts +++ b/src/presentation/http/router/user.ts @@ -63,7 +63,7 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don }); /** - * Get user extensions + * Get user editor tools */ fastify.get('/editor-tools', { config: { @@ -93,9 +93,7 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don }, async (request, reply) => { const userId = request.userId as number; - const userExtensions = await userService.getUserExtensions(userId); - const userEditorToolIds = userExtensions?.editorTools?.map(tools => tools.id) ?? []; - + const userEditorToolIds = await userService.getUserEditorTools(userId); const defaultEditorTools = await editorToolsService.getDefaultEditorTools(); const uniqueDefaultEditorTools = defaultEditorTools.filter(({ id }) => !userEditorToolIds.includes(id)); const userEditorTools = await editorToolsService.getToolsByIds(userEditorToolIds) ?? []; @@ -131,16 +129,16 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don }, }, }, async (request, reply) => { - const editorToolId = request.body.toolId; + const toolId = request.body.toolId; const userId = request.userId as number; await userService.addUserEditorTool({ userId, - editorToolId, + toolId, }); return reply.send({ - data: editorToolId, + data: toolId, }); }); diff --git a/src/presentation/http/schema/User.ts b/src/presentation/http/schema/User.ts index 9061d2f3..4f03ad48 100644 --- a/src/presentation/http/schema/User.ts +++ b/src/presentation/http/schema/User.ts @@ -10,4 +10,11 @@ export const UserSchema = { name: { type: 'string' }, photo: { type: 'string' }, }, + editorTools: { + type: 'array', + description: 'List of editor tools ids installed by user from Marketplace', + items: { + type: 'string', + }, + }, }; diff --git a/src/repository/storage/postgres/orm/sequelize/editorTools.ts b/src/repository/storage/postgres/orm/sequelize/editorTools.ts index 3daad4fa..7dbb62dc 100644 --- a/src/repository/storage/postgres/orm/sequelize/editorTools.ts +++ b/src/repository/storage/postgres/orm/sequelize/editorTools.ts @@ -13,7 +13,7 @@ export class EditorToolModel extends Model, Inf public declare id: EditorTool['id']; /** - * Custom name that uses in editor initiazliation. e.g. 'code' + * Custom name that uses in editor initialization. e.g. 'code' */ public declare name: EditorTool['name']; @@ -112,7 +112,7 @@ export default class UserSequelizeStorage { source, isDefault, }: EditorTool): Promise { - const editorTool = await this.model.create({ + return await this.model.create({ id, name, title, @@ -120,8 +120,6 @@ export default class UserSequelizeStorage { source, isDefault, }); - - return editorTool; } /** @@ -130,36 +128,30 @@ export default class UserSequelizeStorage { * @param editorToolIds - tool ids */ public async getToolsByIds(editorToolIds: EditorTool['id'][]): Promise { - const editorTools = await this.model.findAll({ + return await this.model.findAll({ where: { id: { [Op.in]: editorToolIds, }, }, }); - - return editorTools; } /** * Get all default tools */ public async getDefaultEditorTools(): Promise { - const editorTools = await this.model.findAll({ + return await this.model.findAll({ where: { isDefault: true, }, }); - - return editorTools; } /** * Get all available editor tools */ public async getTools(): Promise { - const editorTools = await EditorToolModel.findAll(); - - return editorTools; + return await EditorToolModel.findAll(); } } diff --git a/src/repository/storage/postgres/orm/sequelize/index.ts b/src/repository/storage/postgres/orm/sequelize/index.ts index 93e3dc10..f8a2c9eb 100644 --- a/src/repository/storage/postgres/orm/sequelize/index.ts +++ b/src/repository/storage/postgres/orm/sequelize/index.ts @@ -28,6 +28,12 @@ export default class SequelizeOrm { this.conn = new Sequelize(this.config.dsn, { logging: databaseLogger.info.bind(databaseLogger), + define: { + /** + * Use snake_case for fields in db, but camelCase in code + */ + underscored: true, + }, }); } diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index 42e24449..bb03092d 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -103,7 +103,7 @@ export default class NoteSequelizeStorage { }, { tableName: this.tableName, sequelize: this.database, - underscored: true, // use snake_case for fields in db + underscored: true, }); } @@ -131,13 +131,11 @@ export default class NoteSequelizeStorage { * @returns { Note } - created note */ public async createNote(options: NoteCreationAttributes): Promise { - const createdNote = await this.model.create({ + return await this.model.create({ publicId: options.publicId, content: options.content, creatorId: options.creatorId, }); - - return createdNote; } /** @@ -170,20 +168,11 @@ export default class NoteSequelizeStorage { * @param id - internal id */ public async getNoteById(id: NoteInternalId): Promise { - const note = await this.model.findOne({ + return await this.model.findOne({ where: { id, }, }); - - /** - * If note not found, return null - */ - if (!note) { - return null; - } - - return note; } /** @@ -213,15 +202,13 @@ export default class NoteSequelizeStorage { * @returns { Promise } note */ public async getNoteListByCreatorId(creatorId: number, offset: number, limit: number): Promise { - const noteList = await this.model.findAll({ + return await this.model.findAll({ offset: offset, limit: limit, where: { creatorId, }, }); - - return noteList; } /** * Gets note by id @@ -256,12 +243,10 @@ export default class NoteSequelizeStorage { * @returns { Promise } found note */ public async getNoteByPublicId(publicId: NotePublicId): Promise { - const note = await this.model.findOne({ + return await this.model.findOne({ where: { publicId, }, }); - - return note; }; } diff --git a/src/repository/storage/postgres/orm/sequelize/noteSettings.ts b/src/repository/storage/postgres/orm/sequelize/noteSettings.ts index 04c8af68..2c36ba01 100644 --- a/src/repository/storage/postgres/orm/sequelize/noteSettings.ts +++ b/src/repository/storage/postgres/orm/sequelize/noteSettings.ts @@ -92,7 +92,6 @@ export default class NoteSettingsSequelizeStorage { tableName: this.tableName, sequelize: this.database, timestamps: false, - underscored: true, // use snake_case for fields in db }); } diff --git a/src/repository/storage/postgres/orm/sequelize/user.ts b/src/repository/storage/postgres/orm/sequelize/user.ts index 242137c8..53585523 100644 --- a/src/repository/storage/postgres/orm/sequelize/user.ts +++ b/src/repository/storage/postgres/orm/sequelize/user.ts @@ -3,7 +3,7 @@ import { fn, col } from 'sequelize'; import { Model, DataTypes } from 'sequelize'; import type Orm from '@repository/storage/postgres/orm/sequelize/index.js'; import type User from '@domain/entities/user.js'; -import type { UserEditorTool } from '@domain/entities/userExtensions.js'; +import type EditorTool from '@domain/entities/editorTools'; /** * Query options for getting user @@ -47,9 +47,9 @@ export interface AddUserToolOptions { userId: User['id']; /** - * Editor tool data + * Editor tool identifier */ - tool: UserEditorTool; + toolId: EditorTool['id']; } /** @@ -62,9 +62,9 @@ interface RemoveUserEditorTool { userId: User['id']; /** - * Editor tool + * Editor tool identifier */ - editorTool: UserEditorTool; + toolId: EditorTool['id']; } /* eslint-disable @typescript-eslint/naming-convention */ @@ -91,7 +91,7 @@ export class UserModel extends Model, InferCreationAt /** * User created at */ - public declare created_at: Date; + public declare createdAt: Date; /** * User photo @@ -99,9 +99,9 @@ export class UserModel extends Model, InferCreationAt public declare photo: CreationOptional; /** - * + * List of tools ids installed by user from Marketplace */ - public declare extensions: CreationOptional; + public declare editorTools: CreationOptional; } /** @@ -149,15 +149,15 @@ export default class UserSequelizeStorage { type: DataTypes.STRING, allowNull: false, }, - created_at: { + createdAt: { type: DataTypes.DATE, allowNull: false, }, photo: { type: DataTypes.STRING, }, - extensions: { - type: DataTypes.JSON, + editorTools: { + type: DataTypes.ARRAY(DataTypes.STRING), }, }, { tableName: this.tableName, @@ -173,14 +173,14 @@ export default class UserSequelizeStorage { */ public async addUserEditorTool({ userId, - tool: editorTool, + toolId, }: AddUserToolOptions): Promise { await this.model.update({ - extensions: fn('array_append', col('editorTools'), editorTool), + editorTools: fn('array_append', col('editor_tools'), toolId), }, { where: { id: userId, - // TODO: Add check to unique editorTool id + // @todo Add check to unique editorTool id }, }); } @@ -192,10 +192,10 @@ export default class UserSequelizeStorage { */ public async removeUserEditorTool({ userId, - editorTool, + toolId, }: RemoveUserEditorTool): Promise { await this.model.update({ - extensions: fn('array_remove', col('editorTools'), editorTool), + editorTools: fn('array_remove', col('editor_tools'), toolId), }, { where: { id: userId, @@ -214,20 +214,12 @@ export default class UserSequelizeStorage { name, photo, }: InsertUserOptions): Promise { - const user = await this.model.create({ + return await this.model.create({ email, name, - created_at: new Date(), + createdAt: new Date(), photo, }); - - return { - id: user.id, - email: user.email, - name: user.name, - createdAt: user.created_at, - photo: user.photo, - }; } /** @@ -274,13 +266,6 @@ export default class UserSequelizeStorage { return null; } - return { - id: user.id, - email: user.email, - name: user.name, - createdAt: user.created_at, - photo: user.photo, - extensions: user.extensions, - }; + return user; } } diff --git a/src/repository/storage/postgres/orm/sequelize/userSession.ts b/src/repository/storage/postgres/orm/sequelize/userSession.ts index 317e1b07..e602b006 100644 --- a/src/repository/storage/postgres/orm/sequelize/userSession.ts +++ b/src/repository/storage/postgres/orm/sequelize/userSession.ts @@ -18,17 +18,17 @@ class UserSessionModel extends Model, InferCre /** * User id */ - public declare user_id: number; + public declare userId: number; /** * Refresh token */ - public declare refresh_token: string; + public declare refreshToken: string; /** * Refresh token expiration date */ - public declare refresh_token_expires_at: Date; + public declare refreshTokenExpiresAt: Date; } @@ -68,7 +68,7 @@ export default class UserSessionSequelizeStorage { autoIncrement: true, primaryKey: true, }, - user_id: { + userId: { type: DataTypes.INTEGER, allowNull: false, references: { @@ -76,12 +76,12 @@ export default class UserSessionSequelizeStorage { key: 'id', }, }, - refresh_token: { + refreshToken: { type: DataTypes.STRING, allowNull: false, unique: true, }, - refresh_token_expires_at: { + refreshTokenExpiresAt: { type: DataTypes.DATE, allowNull: false, }, @@ -101,18 +101,11 @@ export default class UserSessionSequelizeStorage { * @returns { UserSession } created user session */ public async create(userId: number, refreshToken: string, refreshTokenExpiresAt: Date): Promise { - const session = await this.model.create({ - user_id: userId, - refresh_token: refreshToken, - refresh_token_expires_at: refreshTokenExpiresAt, + return await this.model.create({ + userId: userId, + refreshToken: refreshToken, + refreshTokenExpiresAt: refreshTokenExpiresAt, }); - - return { - id: session.id, - userId: session.user_id, - refreshToken: session.refresh_token, - refreshTokenExpiresAt: session.refresh_token_expires_at, - }; } /** @@ -122,20 +115,9 @@ export default class UserSessionSequelizeStorage { * @returns { UserSession | null } found user session */ public async findByToken(token: string): Promise { - const session = await this.model.findOne({ - where: { refresh_token: token }, + return await this.model.findOne({ + where: { refreshToken: token }, }); - - if (!session) { - return null; - } - - return { - id: session.id, - userId: session.user_id, - refreshToken: session.refresh_token, - refreshTokenExpiresAt: session.refresh_token_expires_at, - }; } /** @@ -146,7 +128,9 @@ export default class UserSessionSequelizeStorage { */ public async removeByRefreshToken(refreshToken: string): Promise { await this.model.destroy({ - where: { refresh_token: refreshToken }, + where: { + refreshToken, + }, }); } } diff --git a/src/repository/user.repository.ts b/src/repository/user.repository.ts index 0705c2ef..f3018149 100644 --- a/src/repository/user.repository.ts +++ b/src/repository/user.repository.ts @@ -1,9 +1,9 @@ import type User from '@domain/entities/user.js'; -import type { UserEditorTool } from '@domain/entities/userExtensions.js'; import type UserStorage from '@repository/storage/user.storage.js'; import type GoogleApiTransport from '@repository/transport/google-api/index.js'; import type GetUserInfoResponsePayload from '@repository/transport/google-api/types/GetUserInfoResponsePayload.js'; import type { AddUserToolOptions } from '@repository/storage/postgres/orm/sequelize/user'; +import type EditorTool from '@domain/entities/editorTools'; /** * OAuth provider @@ -27,7 +27,7 @@ interface RemoveUserEditorToolOptions { /** * Editor tool identifier */ - editorToolId: UserEditorTool['id']; + toolId: EditorTool['id']; } /** @@ -127,10 +127,10 @@ export default class UserRepository { * * @param options - identifiers of user and tool */ - public async addUserEditorTool({ userId, tool }: AddUserToolOptions): Promise { + public async addUserEditorTool({ userId, toolId }: AddUserToolOptions): Promise { await this.storage.addUserEditorTool({ userId, - tool, + toolId, }); } @@ -139,22 +139,22 @@ export default class UserRepository { * * @param options - identifiers of user and tool */ - public async removeUserEditorTool({ userId, editorToolId }: RemoveUserEditorToolOptions): Promise { + public async removeUserEditorTool({ userId, toolId }: RemoveUserEditorToolOptions): Promise { const user = await this.getUserById(userId); if (!user) { - throw new Error('There is no user with such userId'); + throw new Error('There is no user with such id'); } - const editorTool = user.extensions?.editorTools?.find(tool => tool.id === editorToolId); + const editorTool = user.editorTools?.find(id => id === toolId); - if (!editorTool) { - throw new Error('User has no tool with such editorToolId'); + if (editorTool === undefined) { + throw new Error('User has no tool with such id'); } await this.storage.removeUserEditorTool({ userId, - editorTool, + toolId: editorTool, }); } } From bd21b03a28ce319f2efb9075f328b10cd94b46ca Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 23 Oct 2023 00:18:27 +0300 Subject: [PATCH 3/7] fix: migrations --- ...r-tools@rename-camelcase-to-underscore.sql | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/migrations/tenant/0009-editor-tools@rename-camelcase-to-underscore.sql b/migrations/tenant/0009-editor-tools@rename-camelcase-to-underscore.sql index 7ee601fd..46c2de25 100644 --- a/migrations/tenant/0009-editor-tools@rename-camelcase-to-underscore.sql +++ b/migrations/tenant/0009-editor-tools@rename-camelcase-to-underscore.sql @@ -1,24 +1,33 @@ -/* - Rename columns in "editor_tools" table: -*/ --- Rename column "isDefault" to "is_default" if exists +-- "editor_tools" table: + +-- Rename column "isDefault" to "is_default" if "isDefault" exists and "is_default" not exists DO $$ BEGIN IF EXISTS(SELECT * FROM information_schema.columns WHERE table_name='editor_tools' and column_name='isDefault') THEN - ALTER TABLE "public"."editor_tools" RENAME COLUMN "isDefault" TO "is_default"; + IF NOT EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='editor_tools' and column_name='is_default') + THEN + ALTER TABLE "public"."editor_tools" RENAME COLUMN "isDefault" TO "is_default"; + END IF; END IF; END $$; --- Rename column "exportName" to "export_name" if exists +-- Rename column "exportName" to "export_name" if "exportName" exists and "export_name" not exists DO $$ BEGIN IF EXISTS(SELECT * FROM information_schema.columns WHERE table_name='editor_tools' and column_name='exportName') THEN - ALTER TABLE "public"."editor_tools" RENAME COLUMN "exportName" TO "export_name"; + IF NOT EXISTS(SELECT * + FROM information_schema.columns + WHERE table_name='editor_tools' and column_name='export_name') + THEN + ALTER TABLE "public"."editor_tools" RENAME COLUMN "exportName" TO "export_name"; + END IF; END IF; END $$; From 648fee11599830494e7eda5095c61c7aae503dba Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 23 Oct 2023 00:21:19 +0300 Subject: [PATCH 4/7] rm unused option --- src/repository/storage/postgres/orm/sequelize/note.ts | 1 - src/repository/storage/postgres/orm/sequelize/teams.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index bb03092d..03f40a3e 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -103,7 +103,6 @@ export default class NoteSequelizeStorage { }, { tableName: this.tableName, sequelize: this.database, - underscored: true, }); } diff --git a/src/repository/storage/postgres/orm/sequelize/teams.ts b/src/repository/storage/postgres/orm/sequelize/teams.ts index ee241656..d6df080a 100644 --- a/src/repository/storage/postgres/orm/sequelize/teams.ts +++ b/src/repository/storage/postgres/orm/sequelize/teams.ts @@ -104,7 +104,6 @@ export default class TeamsSequelizeStorage { tableName: this.tableName, sequelize: this.database, timestamps: false, - underscored: true, // use snake_case for fields in db }); } From 7c3d21260e7ec75ad531627f5e065612351998e2 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 23 Oct 2023 02:48:12 +0300 Subject: [PATCH 5/7] chore: move bis-logic from ui do domain, decouple domains --- src/domain/index.ts | 10 +++++++--- src/domain/service/editorTools.ts | 9 +++++---- src/domain/service/shared/README.md | 6 ++++++ src/domain/service/shared/editorTools.ts | 19 +++++++++++++++++++ src/domain/service/shared/index.ts | 5 +++++ src/domain/service/user.ts | 18 +++++++++++++++--- src/presentation/http/router/user.ts | 12 ++---------- src/repository/editorTools.repository.ts | 4 ++-- src/repository/note.repository.ts | 2 -- .../postgres/orm/sequelize/editorTools.ts | 2 +- 10 files changed, 62 insertions(+), 25 deletions(-) create mode 100644 src/domain/service/shared/README.md create mode 100644 src/domain/service/shared/editorTools.ts create mode 100644 src/domain/service/shared/index.ts diff --git a/src/domain/index.ts b/src/domain/index.ts index 8e88151c..c061e04e 100644 --- a/src/domain/index.ts +++ b/src/domain/index.ts @@ -63,10 +63,14 @@ export function init(repositories: Repositories, appConfig: AppConfig): DomainSe repositories.userSessionRepository ); - const userService = new UserService(repositories.userRepository); - - const aiService = new AIService(repositories.aiRepository); const editorToolsService = new EditorToolsService(repositories.editorToolsRepository); + const userService = new UserService(repositories.userRepository, { + editorTools: editorToolsService, + /** + * @todo find a way how to resolve circular dependency + */ + }); + const aiService = new AIService(repositories.aiRepository); return { noteService, diff --git a/src/domain/service/editorTools.ts b/src/domain/service/editorTools.ts index c956e0f8..c0a14282 100644 --- a/src/domain/service/editorTools.ts +++ b/src/domain/service/editorTools.ts @@ -1,11 +1,12 @@ import type EditorToolsRepository from '@repository/editorTools.repository.js'; import type EditorTool from '@domain/entities/editorTools.js'; import { createEditorToolId } from '@infrastructure/utils/id.js'; +import type EditorToolsServiceSharedMethods from './shared/editorTools.js'; /** * Editor tools service */ -export default class EditorToolsService { +export default class EditorToolsService implements EditorToolsServiceSharedMethods { /** * User repository instance */ @@ -37,10 +38,10 @@ export default class EditorToolsService { } /** - * Get all default tools + * Return tools that are available at Editor by default */ - public async getDefaultEditorTools(): Promise { - return await this.repository.getDefaultEditorTools(); + public async getDefaultTools(): Promise { + return await this.repository.getDefaultTools(); } /** diff --git a/src/domain/service/shared/README.md b/src/domain/service/shared/README.md new file mode 100644 index 00000000..92c8f628 --- /dev/null +++ b/src/domain/service/shared/README.md @@ -0,0 +1,6 @@ +# Shared Domain Services + +Sometimes you may want to call some domain method from other domain. We can inject them in constructor, but it creates a direct dependency. + +One way do decouple domains is to create a Shared Interfaces — domain will "expose" some public methods though it. You can call it "Contract". +So dependant domain will depend on it instead of direct dependency. diff --git a/src/domain/service/shared/editorTools.ts b/src/domain/service/shared/editorTools.ts new file mode 100644 index 00000000..8f1d63a4 --- /dev/null +++ b/src/domain/service/shared/editorTools.ts @@ -0,0 +1,19 @@ +import type EditorTool from '@domain/entities/editorTools'; + +/** + * Which methods of Domain can be used by other domains + * Uses to decouple domains from each other + */ +export default interface EditorToolsServiceSharedMethods { + /** + * Return tools that are available at Editor by default + */ + getDefaultTools(): Promise; + + /** + * Get bunch of editor tools by their ids + * + * @param ids - tool ids to resolve + */ + getToolsByIds(ids: EditorTool['id'][]): Promise; +} diff --git a/src/domain/service/shared/index.ts b/src/domain/service/shared/index.ts new file mode 100644 index 00000000..11364d8f --- /dev/null +++ b/src/domain/service/shared/index.ts @@ -0,0 +1,5 @@ +import type EditorToolsServiceSharedMethods from './editorTools'; + +export type SharedDomainMethods = { + editorTools: EditorToolsServiceSharedMethods; +}; diff --git a/src/domain/service/user.ts b/src/domain/service/user.ts index c58d3b93..b54a0ea1 100644 --- a/src/domain/service/user.ts +++ b/src/domain/service/user.ts @@ -2,6 +2,7 @@ import type UserRepository from '@repository/user.repository.js'; import { Provider } from '@repository/user.repository.js'; import type User from '@domain/entities/user.js'; import type EditorTool from '@domain/entities/editorTools'; +import type { SharedDomainMethods } from './shared/index.js'; export { Provider @@ -20,8 +21,9 @@ export default class UserService { * User service constructor * * @param repository - user repository instance + * @param shared - shared domains */ - constructor(repository: UserRepository) { + constructor(repository: UserRepository, private readonly shared: SharedDomainMethods) { this.repository = repository; } @@ -51,14 +53,24 @@ export default class UserService { * * @param userId - user unique identifier */ - public async getUserEditorTools(userId: User['id']): Promise { + public async getUserEditorTools(userId: User['id']): Promise { const user = await this.getUserById(userId); if (user === null) { throw new Error('User not found'); } - return user.editorTools ?? []; + const userToolsIds = user.editorTools ?? []; + const defaultTools = await this.shared.editorTools.getDefaultTools(); + const uniqueDefaultEditorTools = defaultTools.filter(({ id }) => !userToolsIds.includes(id)); + const userTools = await this.shared.editorTools.getToolsByIds(userToolsIds) ?? []; + + /** + * Combine user tools and default tools + * + * @todo load tools in notes service + */ + return [...userTools, ...uniqueDefaultEditorTools]; } /** diff --git a/src/presentation/http/router/user.ts b/src/presentation/http/router/user.ts index f2aa2de8..965f8076 100644 --- a/src/presentation/http/router/user.ts +++ b/src/presentation/http/router/user.ts @@ -30,7 +30,6 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don * Manage user data */ const userService = opts.userService; - const editorToolsService = opts.editorToolsService; /** * Get user by session @@ -93,17 +92,10 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don }, async (request, reply) => { const userId = request.userId as number; - const userEditorToolIds = await userService.getUserEditorTools(userId); - const defaultEditorTools = await editorToolsService.getDefaultEditorTools(); - const uniqueDefaultEditorTools = defaultEditorTools.filter(({ id }) => !userEditorToolIds.includes(id)); - const userEditorTools = await editorToolsService.getToolsByIds(userEditorToolIds) ?? []; - - // Combine user tools and default tools - // TODO: load tools in notes service - const mergedTools = [...userEditorTools, ...uniqueDefaultEditorTools]; + const tools = await userService.getUserEditorTools(userId); return reply.send({ - data: mergedTools, + data: tools, }); }); diff --git a/src/repository/editorTools.repository.ts b/src/repository/editorTools.repository.ts index bbe15e68..c29dbe12 100644 --- a/src/repository/editorTools.repository.ts +++ b/src/repository/editorTools.repository.ts @@ -37,8 +37,8 @@ export default class EditorToolsRepository { /** * Get all default tools */ - public async getDefaultEditorTools(): Promise { - return await this.storage.getDefaultEditorTools(); + public async getDefaultTools(): Promise { + return await this.storage.getDefaultTools(); } /** diff --git a/src/repository/note.repository.ts b/src/repository/note.repository.ts index e2baa164..73677924 100644 --- a/src/repository/note.repository.ts +++ b/src/repository/note.repository.ts @@ -1,6 +1,5 @@ import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js'; import type NoteStorage from '@repository/storage/note.storage.js'; -import type { NoteList } from '@domain/entities/noteList.js'; /** * Repository allows accessing data from business-logic (domain) level @@ -85,7 +84,6 @@ export default class NoteRepository { * @param id - note creator id * @param offset - number of skipped notes * @param limit - number of notes to get - * @returns { Promise } note */ public async getNoteListByCreatorId(id: number, offset: number, limit: number): Promise { return await this.storage.getNoteListByCreatorId(id, offset, limit); diff --git a/src/repository/storage/postgres/orm/sequelize/editorTools.ts b/src/repository/storage/postgres/orm/sequelize/editorTools.ts index 7dbb62dc..a431bb5d 100644 --- a/src/repository/storage/postgres/orm/sequelize/editorTools.ts +++ b/src/repository/storage/postgres/orm/sequelize/editorTools.ts @@ -140,7 +140,7 @@ export default class UserSequelizeStorage { /** * Get all default tools */ - public async getDefaultEditorTools(): Promise { + public async getDefaultTools(): Promise { return await this.model.findAll({ where: { isDefault: true, From 239ec7c3aca2fd7912027b199236cbbb4765ef15 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 23 Oct 2023 02:54:42 +0300 Subject: [PATCH 6/7] Update README.md --- src/domain/service/shared/README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/domain/service/shared/README.md b/src/domain/service/shared/README.md index 92c8f628..f49eb8a7 100644 --- a/src/domain/service/shared/README.md +++ b/src/domain/service/shared/README.md @@ -4,3 +4,27 @@ Sometimes you may want to call some domain method from other domain. We can inje One way do decouple domains is to create a Shared Interfaces — domain will "expose" some public methods though it. You can call it "Contract". So dependant domain will depend on it instead of direct dependency. + + +## Example + +```ts +interface DomainASharedMethods { + someMethodA: () => void; +} + +export type SharedDomainMethods = { + editorTools: DomainASharedMethods; +}; + + +class DomainA implements DomainASharedMethods { + public someMethodA (){} +} + +class DomainB { + constructor(private readonly shared: SharedDomainMethods) { + this.shared.someMethodA(); // here we call method of Domain A, but without direct dependency + } +} +``` From fa36e2e9433042de8f28054ae8edc85a48f7880a Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 23 Oct 2023 12:11:42 +0300 Subject: [PATCH 7/7] fix readme --- src/domain/service/shared/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/service/shared/README.md b/src/domain/service/shared/README.md index f49eb8a7..b7d2d0f0 100644 --- a/src/domain/service/shared/README.md +++ b/src/domain/service/shared/README.md @@ -14,7 +14,7 @@ interface DomainASharedMethods { } export type SharedDomainMethods = { - editorTools: DomainASharedMethods; + domainA: DomainASharedMethods; }; @@ -24,7 +24,7 @@ class DomainA implements DomainASharedMethods { class DomainB { constructor(private readonly shared: SharedDomainMethods) { - this.shared.someMethodA(); // here we call method of Domain A, but without direct dependency + this.shared.domainA.someMethodA(); // here we call method of Domain A, but without direct dependency } } ```