diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 9079ed3d..889b85c8 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -18,7 +18,7 @@ module.exports = { 'format': ['camelCase', 'PascalCase'], 'filter': { // Allow "2xx" as a property name, used in the API response schema - 'regex': '^(2xx)$', + 'regex': '^(2xx|2\d{2}|application\/json)$', 'match': false, }, }, diff --git a/src/domain/entities/editorTools.ts b/src/domain/entities/editorTools.ts index 3455a2e4..e272ec9f 100644 --- a/src/domain/entities/editorTools.ts +++ b/src/domain/entities/editorTools.ts @@ -3,20 +3,30 @@ */ export default interface EditorTool { /** - * Unique identifier of the tool + * Unique identifier of the tool. Nano-ID */ id: string; /** - * User-friendly plugin title + * Technical name of the tool, like 'header', 'list', 'linkTool' */ name: string; + /** + * User-friendly plugin title + */ + title: string; + /** * Name of the tool class. Since it's imported globally, * we need the class name to properly connect the tool to the editor */ - class: string; + exportName: string; + + /** + * Is plugin included by default in the editor + */ + isDefault?: boolean; /** * Source of the tool to get it's code diff --git a/src/domain/service/editorTools.ts b/src/domain/service/editorTools.ts index 7b39f956..a25e51d2 100644 --- a/src/domain/service/editorTools.ts +++ b/src/domain/service/editorTools.ts @@ -1,5 +1,6 @@ import type EditorToolsRepository from '@repository/editorTools.repository.js'; import type EditorTool from '@domain/entities/editorTools.js'; +import { createEditorToolId } from '@infrastructure/utils/id.js'; /** * Editor tools service @@ -26,13 +27,25 @@ export default class EditorToolsService { return await this.repository.getTools(); } + /** + * Get bunch of editor tools by their ids + * + * @param editorToolIds - tool ids + */ + public async getToolsByIds(editorToolIds: EditorTool['id'][] ): Promise { + return await this.repository.getToolsByIds(editorToolIds); + } + /** * Adding custom editor tool * - * @param tool - all data about the editor plugin - * @returns {Promise} editor tool data + * @param editorTool - all data about the editor plugin + * @returns {Promise} editor tool data */ - public async addTool(tool: EditorTool): Promise { - return await this.repository.addTools(tool); + public async addTool(editorTool: Omit): Promise { + return await this.repository.addTool({ + id: createEditorToolId(), + ...editorTool, + }); } } diff --git a/src/domain/service/user.ts b/src/domain/service/user.ts index 33fb3335..f734bdb3 100644 --- a/src/domain/service/user.ts +++ b/src/domain/service/user.ts @@ -1,7 +1,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 { UserEditorTool } from '@domain/entities/userExtensions.js'; +import type EditorTool from '@domain/entities/editorTools'; export { Provider @@ -47,13 +47,34 @@ export default class UserService { } /** - * Get installed user tools + * Get user extensions that contains only editoTools for now + * TODO: Simplify extenisons * * @param userId - user unique identifier */ - public async getUserEditorTools(userId: User['id']): Promise { + public async getUserExtensions(userId: User['id']): Promise { const user = await this.getUserById(userId); - return user?.extensions?.editorTools; + return user?.extensions ?? {}; + } + + /** + * Adds editor tool to user settings by its id + * + * @param options - user id & editor tool + */ + public async addUserEditorTool({ + userId, + editorToolId, + }: { + userId: User['id'], + editorToolId: EditorTool['id'], + }): Promise { + return await this.repository.addUserEditorTool({ + userId, + tool: { + id: editorToolId, + }, + }); } } diff --git a/src/infrastructure/utils/id.ts b/src/infrastructure/utils/id.ts index e7c667ff..040780da 100644 --- a/src/infrastructure/utils/id.ts +++ b/src/infrastructure/utils/id.ts @@ -9,3 +9,13 @@ import { nanoid } from 'nanoid'; export function createPublicId(length: number = 10): string { return nanoid(length); } + +/** + * Create unique identifier for editor tools + * Used in editor tools and user settings + * + * @param length - id length + */ +export function createEditorToolId(length: number = 8): string { + return nanoid(length); +} diff --git a/src/presentation/http/http-api.ts b/src/presentation/http/http-api.ts index 0b371cac..6027db9e 100644 --- a/src/presentation/http/http-api.ts +++ b/src/presentation/http/http-api.ts @@ -23,6 +23,7 @@ import Policies from './policies/index.js'; import type { RequestParams, Response } from '@presentation/api.interface.js'; import NoteSettingsRouter from './router/noteSettings.js'; import NoteListRouter from '@presentation/http/router/noteList.js'; +import { EditorToolSchema } from './schema/EditorTool.js'; const appServerLogger = getLogger('appServer'); @@ -132,8 +133,8 @@ export default class HttpApi implements Api { url: 'http://localhost:1337', description: 'Localhost environment', }, { - url: 'https://notex.so', - description: 'Stage environment', + url: 'https://api.notex.so', + description: 'Production environment', } ], components: { securitySchemes: { @@ -142,12 +143,11 @@ export default class HttpApi implements Api { description: 'Provied authorization uses OAuth 2 with Google', flows: { authorizationCode: { - authorizationUrl: 'https://notex.so/oauth/google/login', + authorizationUrl: 'https://api.notex.so/oauth/google/login', scopes: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'notes_management': 'Create, read, update and delete notes', + 'notesManagement': 'Create, read, update and delete notes', }, - tokenUrl: '', + tokenUrl: 'https://api.notex.so/oauth/google/callback', }, }, }, @@ -218,6 +218,7 @@ export default class HttpApi implements Api { await this.server?.register(UserRouter, { prefix: '/user', userService: domainServices.userService, + editorToolsService: domainServices.editorToolsService, }); await this.server?.register(AIRouter, { @@ -266,6 +267,7 @@ export default class HttpApi implements Api { private addSchema(): void { this.server?.addSchema(UserSchema); this.server?.addSchema(NoteSchema); + this.server?.addSchema(EditorToolSchema); } /** diff --git a/src/presentation/http/router/editorTools.ts b/src/presentation/http/router/editorTools.ts index 359c1e0a..0ee1a815 100644 --- a/src/presentation/http/router/editorTools.ts +++ b/src/presentation/http/router/editorTools.ts @@ -29,7 +29,27 @@ const EditorToolsRouter: FastifyPluginCallback = (fast /** * Get all avaiable editor tools */ - fastify.get('/all', async (_, reply) => { + fastify.get('/all', { + schema: { + response: { + '2xx': { + description: 'Editor tool fields', + content: { + 'application/json': { + schema: { + data: { + type: 'array', + items: { + $ref: 'EditorToolSchema', + }, + }, + }, + }, + }, + }, + }, + }, + }, async (_, reply) => { const tools = await editorToolsService.getTools(); return reply.send({ @@ -40,7 +60,29 @@ const EditorToolsRouter: FastifyPluginCallback = (fast /** * Add editor tool to the library of all tools */ - fastify.post<{ Body: EditorTool }>('/add-tool', async (request, reply) => { + fastify.post<{ + Body: EditorTool + }>('/add-tool', { + schema: { + body: { + $ref: 'EditorToolSchema', + }, + response: { + '2xx': { + description: 'Editor tool fields', + content: { + 'application/json': { + schema: { + data: { + $ref: 'EditorToolSchema', + }, + }, + }, + }, + }, + }, + }, + }, async (request, reply) => { const editorTool = request.body; const tool = await editorToolsService.addTool(editorTool); diff --git a/src/presentation/http/router/user.ts b/src/presentation/http/router/user.ts index a5312c15..2b1e120e 100644 --- a/src/presentation/http/router/user.ts +++ b/src/presentation/http/router/user.ts @@ -1,6 +1,7 @@ import type { FastifyPluginCallback } from 'fastify'; import type UserService from '@domain/service/user.js'; import type User from '@domain/entities/user.js'; +import type EditorToolsService from '@domain/service/editorTools'; /** * Interface for the user router @@ -10,6 +11,11 @@ interface UserRouterOptions { * User service instance */ userService: UserService, + + /** + * Service editor tool + */ + editorToolsService: EditorToolsService, } /** @@ -24,6 +30,7 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don * Manage user data */ const userService = opts.userService; + const editorToolsService = opts.editorToolsService; /** * Get user by session @@ -64,16 +71,72 @@ const UserRouter: FastifyPluginCallback = (fastify, opts, don 'authRequired', ], }, + schema: { + response: { + '2xx': { + description: 'Editor tool fields', + content: { + 'application/json': { + schema: { + data: { + type: 'array', + items: { + $ref: 'EditorToolSchema', + }, + }, + }, + }, + }, + }, + }, + }, }, async (request, reply) => { const userId = request.userId as number; - const editorTools = await userService.getUserEditorTools(userId) ?? []; + const userExtensions = await userService.getUserExtensions(userId); + const userEditorToolIds = userExtensions?.editorTools?.map(tools => tools.id) ?? []; + const editorTools = await editorToolsService.getToolsByIds(userEditorToolIds) ?? []; return reply.send({ data: editorTools, }); }); + /** + * Add editor tool to user extensions. + * These editor tools are used when creating new notes. + * Tool is linked by it's id. + */ + fastify.post<{ + Body: { toolId: string } + }>('/editor-tools', { + config: { + policy: [ + 'authRequired', + ], + }, + schema: { + body: { + toolId: { + type: 'string', + description: 'Unique editor tool id', + }, + }, + }, + }, async (request, reply) => { + const editorToolId = request.body.toolId; + const userId = request.userId as number; + + await userService.addUserEditorTool({ + userId, + editorToolId, + }); + + return reply.send({ + data: editorToolId, + }); + }); + done(); }; diff --git a/src/presentation/http/schema/EditorTool.ts b/src/presentation/http/schema/EditorTool.ts new file mode 100644 index 00000000..96631bd4 --- /dev/null +++ b/src/presentation/http/schema/EditorTool.ts @@ -0,0 +1,38 @@ +export const EditorToolSchema = { + $id: 'EditorToolSchema', + type: 'object', + properties: { + id: { + type: 'string', + readOnly: true, + description: 'Unique tool id', + }, + name: { + type: 'string', + description: 'Plugin id that editor will use, e.g. "warning", "list", "linkTool"', + }, + title: { + type: 'string', + description: 'User-friendly name that will be shown in marketplace, .e.g "Warning tool 3000"', + }, + exportName: { + type: 'string', + description: 'Name of the plugin\'s class, e.g. "LinkTool", "Checklist", "Header"', + }, + isDefault: { + type: 'boolean', + description: 'Is plugin included by default in the editor', + default: false, + }, + source: { + type: 'object', + properties: { + cdn: { + type: 'string', + description: 'Tool URL in content delivery network', + }, + }, + }, + }, +}; + diff --git a/src/repository/editorTools.repository.ts b/src/repository/editorTools.repository.ts index c5e9e096..9a20d359 100644 --- a/src/repository/editorTools.repository.ts +++ b/src/repository/editorTools.repository.ts @@ -17,12 +17,23 @@ export default class EditorToolsRepository { /** * @param editorTool - all editor tool data */ - public async addTools(editorTool: EditorTool): Promise { + public async addTool(editorTool: EditorTool): Promise { const createdEditorTool = await this.storage.addTool(editorTool); return createdEditorTool; } + /** + * Get bunch of tools by their ids + * + * @param editorToolIds - unique tool ids + */ + public async getToolsByIds(editorToolIds: EditorTool['id'][]): Promise { + const tools = await this.storage.getToolsByIds(editorToolIds); + + return tools; + } + /** * 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 2dc95469..f20d3c17 100644 --- a/src/repository/storage/postgres/orm/sequelize/editorTools.ts +++ b/src/repository/storage/postgres/orm/sequelize/editorTools.ts @@ -1,34 +1,31 @@ import type { Sequelize, InferAttributes, InferCreationAttributes } from 'sequelize'; -import { Model, DataTypes } from 'sequelize'; +import { Model, DataTypes, Op } from 'sequelize'; import type Orm from '@repository/storage/postgres/orm/sequelize/index.js'; import type EditorTool from '@domain/entities/editorTools.js'; - -interface AddToolOptions { - id: EditorTool['id']; - name: EditorTool['name']; - class: EditorTool['class']; - source: EditorTool['source']; -} - /** * Class representing an EditorTool model in database */ export class EditorToolModel extends Model, InferCreationAttributes> { /** - * Editor tool unique id + * Editor tool unique id, Nano-ID */ public declare id: EditorTool['id']; /** - * Editor tool title + * Custom name that uses in editor initiazliation. e.g. 'code' */ public declare name: EditorTool['name']; /** - * User tool class name + * Editor tool title. e.g. 'Code tool 3000' */ - public declare class: EditorTool['class']; + public declare title: EditorTool['title']; + + /** + * User tool class name. e.g. 'CodeTool' + */ + public declare exportName: EditorTool['exportName']; /** * Editor tool sources @@ -76,7 +73,11 @@ export default class UserSequelizeStorage { type: DataTypes.STRING, allowNull: false, }, - class: { + title: { + type: DataTypes.STRING, + allowNull: false, + }, + exportName: { type: DataTypes.STRING, allowNull: false, }, @@ -97,19 +98,38 @@ export default class UserSequelizeStorage { public async addTool({ id, name, - class: editorToolClass, + title, + exportName, source, - }: AddToolOptions): Promise { + }: EditorTool): Promise { const editorTool = await this.model.create({ id, name, - class: editorToolClass, + title, + exportName, source, }); return editorTool; } + /** + * Get bunch of tools by their ids + * + * @param editorToolIds - tool ids + */ + public async getToolsByIds(editorToolIds: EditorTool['id'][]): Promise { + const editorTools = await this.model.findAll({ + where: { + id: { + [Op.in]: editorToolIds, + }, + }, + }); + + return editorTools; + } + /** * Get all available editor tools */ diff --git a/src/repository/storage/postgres/orm/sequelize/user.ts b/src/repository/storage/postgres/orm/sequelize/user.ts index 10f7b54a..242137c8 100644 --- a/src/repository/storage/postgres/orm/sequelize/user.ts +++ b/src/repository/storage/postgres/orm/sequelize/user.ts @@ -49,7 +49,7 @@ export interface AddUserToolOptions { /** * Editor tool data */ - editorTool: UserEditorTool; + tool: UserEditorTool; } /** @@ -173,7 +173,7 @@ export default class UserSequelizeStorage { */ public async addUserEditorTool({ userId, - editorTool, + tool: editorTool, }: AddUserToolOptions): Promise { await this.model.update({ extensions: fn('array_append', col('editorTools'), editorTool), @@ -280,6 +280,7 @@ export default class UserSequelizeStorage { name: user.name, createdAt: user.created_at, photo: user.photo, + extensions: user.extensions, }; } } diff --git a/src/repository/user.repository.ts b/src/repository/user.repository.ts index cd7025c1..0705c2ef 100644 --- a/src/repository/user.repository.ts +++ b/src/repository/user.repository.ts @@ -127,9 +127,11 @@ export default class UserRepository { * * @param options - identifiers of user and tool */ - public async addUserEditorTool({ userId, editorTool }: AddUserToolOptions): Promise { - await this.storage.addUserEditorTool({ userId, - editorTool }); + public async addUserEditorTool({ userId, tool }: AddUserToolOptions): Promise { + await this.storage.addUserEditorTool({ + userId, + tool, + }); } /** @@ -150,7 +152,9 @@ export default class UserRepository { throw new Error('User has no tool with such editorToolId'); } - await this.storage.removeUserEditorTool({ userId, - editorTool }); + await this.storage.removeUserEditorTool({ + userId, + editorTool, + }); } }