From 1723a13ced141b7072f56f5328687ce6befbbaaa Mon Sep 17 00:00:00 2001 From: Anton Reshetov Date: Wed, 19 Mar 2025 11:50:20 +0300 Subject: [PATCH 1/2] refactor(api): use PATCH method to update snippets --- src/main/api/dto/snippets.ts | 17 ++- src/main/api/routes/snippets.ts | 129 ++++++++++++------ .../components/sidebar/folders/TreeNode.vue | 8 +- src/renderer/components/snippet/Item.vue | 18 +-- src/renderer/composables/useSnippets.ts | 6 +- src/renderer/services/api/generated/index.ts | 34 +++-- 6 files changed, 124 insertions(+), 88 deletions(-) diff --git a/src/main/api/dto/snippets.ts b/src/main/api/dto/snippets.ts index 0c668d83..5434f663 100644 --- a/src/main/api/dto/snippets.ts +++ b/src/main/api/dto/snippets.ts @@ -7,11 +7,11 @@ const snippetsAdd = t.Object({ }) const snippetsUpdate = t.Object({ - ...snippetsAdd.properties, - folderId: t.Union([t.Number(), t.Null()]), - description: t.Union([t.String(), t.Null()]), - isDeleted: t.Number({ minimum: 0, maximum: 1 }), - isFavorites: t.Number({ minimum: 0, maximum: 1 }), + name: t.Optional(t.String()), + folderId: t.Optional(t.Union([t.Number(), t.Null()])), + description: t.Optional(t.Union([t.String(), t.Null()])), + isDeleted: t.Optional(t.Number({ minimum: 0, maximum: 1 })), + isFavorites: t.Optional(t.Number({ minimum: 0, maximum: 1 })), }) const snippetContentsAdd = t.Object({ @@ -20,6 +20,12 @@ const snippetContentsAdd = t.Object({ language: t.String(), // TODO: enum }) +const snippetContentsUpdate = t.Object({ + label: t.Optional(t.String()), + value: t.Optional(t.Union([t.String(), t.Null()])), + language: t.Optional(t.String()), // TODO: enum +}) + const snippetItem = t.Object({ id: t.Number(), name: t.String(), @@ -54,6 +60,7 @@ const snippetsResponse = t.Array(snippetItem) export const snippetsDTO = new Elysia().model({ snippetContentsAdd, + snippetContentsUpdate, snippetsAdd, snippetsUpdate, snippetsQuery: t.Object({ diff --git a/src/main/api/routes/snippets.ts b/src/main/api/routes/snippets.ts index 8cd8329b..a3165a3b 100644 --- a/src/main/api/routes/snippets.ts +++ b/src/main/api/routes/snippets.ts @@ -205,34 +205,57 @@ app }, ) // Обновление сниппета - .put( + .patch( '/:id', ({ params, body, error }) => { const { id } = params - const { name, description, folderId, isFavorites, isDeleted } = body - const stmt = db.prepare(` - UPDATE snippets SET - name = ?, - description = ?, - folderId = ?, - isFavorites = ?, - isDeleted = ?, - updatedAt = ? - WHERE id = ? - `) + const updateFields: string[] = [] + const updateParams: any[] = [] + + if ('name' in body) { + updateFields.push('name = ?') + updateParams.push(body.name) + } + + if ('description' in body) { + updateFields.push('description = ?') + updateParams.push(body.description) + } + + if ('folderId' in body) { + updateFields.push('folderId = ?') + updateParams.push(body.folderId) + } + + if ('isFavorites' in body) { + updateFields.push('isFavorites = ?') + updateParams.push(body.isFavorites) + } + + if ('isDeleted' in body) { + updateFields.push('isDeleted = ?') + updateParams.push(body.isDeleted) + } + + if (updateFields.length === 0) { + return error(400, { message: 'Need at least one field to update' }) + } + + updateFields.push('updatedAt = ?') const now = new Date().getTime() - const result = stmt.run( - name, - description, - folderId, - isFavorites, - isDeleted, - now, - id, - ) + updateParams.push(now) + updateParams.push(id) + + const stmt = db.prepare(` + UPDATE snippets SET + ${updateFields.join(', ')} + WHERE id = ? + `) + + const result = stmt.run(...updateParams) if (!result.changes) { return error(404, { message: 'Snippet not found' }) @@ -248,47 +271,65 @@ app }, ) // Обновление содержимого сниппета - .put( + .patch( '/:id/contents/:contentId', ({ params, body, error }) => { const { id, contentId } = params - const { label, value, language } = body - // обновляем updateAt для сниппета - const snippetsStmt = db.prepare(` - UPDATE snippets SET updatedAt = ? WHERE id = ? - `) + const updateFields: string[] = [] + const updateParams: any[] = [] - const now = new Date().getTime() - const snippetResult = snippetsStmt.run(now, id) + if ('label' in body) { + updateFields.push('label = ?') + updateParams.push(body.label) + } - if (!snippetResult.changes) { - return error(404, { message: 'Snippet not found' }) + if ('value' in body) { + updateFields.push('value = ?') + updateParams.push(body.value) + } + + if ('language' in body) { + updateFields.push('language = ?') + updateParams.push(body.language) } + if (updateFields.length === 0) { + return error(400, { message: 'Need at least one field to update' }) + } + + updateParams.push(contentId) + const contentsStmt = db.prepare(` - UPDATE snippet_contents SET - label = ?, - value = ?, - language = ? - WHERE id = ? - `) + UPDATE snippet_contents SET + ${updateFields.join(', ')} + WHERE id = ? + `) - const contentsResult = contentsStmt.run( - label, - value, - language, - contentId, - ) + const contentsResult = contentsStmt.run(...updateParams) if (!contentsResult.changes) { return error(404, { message: 'Snippet content not found' }) } + // Обновляем дату сниппета только если были реальные изменения в данных + if (contentsResult.changes > 0) { + const snippetsStmt = db.prepare(` + UPDATE snippets SET updatedAt = ? WHERE id = ? + `) + + const now = new Date().getTime() + const snippetResult = snippetsStmt.run(now, id) + + if (!snippetResult.changes) { + return error(404, { message: 'Snippet not found' }) + } + } + return { message: 'Snippet content updated' } }, { - body: 'snippetContentsAdd', + body: 'snippetContentsUpdate', detail: { tags: ['Snippets'], }, diff --git a/src/renderer/components/sidebar/folders/TreeNode.vue b/src/renderer/components/sidebar/folders/TreeNode.vue index f2ad3e17..48fbcbee 100644 --- a/src/renderer/components/sidebar/folders/TreeNode.vue +++ b/src/renderer/components/sidebar/folders/TreeNode.vue @@ -181,13 +181,7 @@ async function onDrop(e: DragEvent) { } const ids = snippets.map(s => s.id) - const data = snippets.map(s => ({ - name: s.name, - folderId: props.node.id, - description: s.description, - isDeleted: s.isDeleted, - isFavorites: s.isFavorites, - })) + const data = snippets.map(() => ({ folderId: props.node.id })) await updateSnippets(ids, data) diff --git a/src/renderer/components/snippet/Item.vue b/src/renderer/components/snippet/Item.vue index 42e3ba20..612fd22c 100644 --- a/src/renderer/components/snippet/Item.vue +++ b/src/renderer/components/snippet/Item.vue @@ -92,21 +92,12 @@ async function onDelete() { } else { // Мягкое удаление - const snippetsToSoftDelete = displayedSnippets.value?.filter(s => - selectedSnippetIds.value.includes(s.id), - ) - const snippetIds = snippetsToSoftDelete?.map(s => s.id) - const snippetsData = snippetsToSoftDelete?.map(s => ({ - name: s.name, - folderId: s.folder?.id || null, - description: s.description, + const snippetsData = selectedSnippetIds.value?.map(() => ({ + folderId: null, isDeleted: 1, - isFavorites: s.isFavorites, })) - if (snippetIds && snippetsData) { - await updateSnippets(snippetIds, snippetsData) - } + await updateSnippets(selectedSnippetIds.value, snippetsData) } } else if (props.snippet.isDeleted) { @@ -115,11 +106,8 @@ async function onDelete() { else { // Мягкое удаление await updateSnippet(props.snippet.id, { - name: props.snippet.name, folderId: null, - description: props.snippet.description, isDeleted: 1, - isFavorites: props.snippet.isFavorites, }) } diff --git a/src/renderer/composables/useSnippets.ts b/src/renderer/composables/useSnippets.ts index ce5ed699..4c1531c7 100644 --- a/src/renderer/composables/useSnippets.ts +++ b/src/renderer/composables/useSnippets.ts @@ -168,13 +168,13 @@ async function createSnippetContent(snippetId: number) { } async function updateSnippet(snippetId: number, data: SnippetsUpdate) { - await api.snippets.putSnippetsById(String(snippetId), data) + await api.snippets.patchSnippetsById(String(snippetId), data) await getSnippets(queryByLibraryOrFolderOrSearch.value) } async function updateSnippets(snippetIds: number[], data: SnippetsUpdate[]) { for (const [index, snippetId] of snippetIds.entries()) { - await api.snippets.putSnippetsById(String(snippetId), data[index]) + await api.snippets.patchSnippetsById(String(snippetId), data[index]) } await getSnippets(queryByLibraryOrFolderOrSearch.value) } @@ -184,7 +184,7 @@ async function updateSnippetContent( contentId: number, data: SnippetContentsAdd, ) { - await api.snippets.putSnippetsByIdContentsByContentId( + await api.snippets.patchSnippetsByIdContentsByContentId( String(snippetId), String(contentId), data, diff --git a/src/renderer/services/api/generated/index.ts b/src/renderer/services/api/generated/index.ts index cb82b306..6cd8bc47 100644 --- a/src/renderer/services/api/generated/index.ts +++ b/src/renderer/services/api/generated/index.ts @@ -15,6 +15,12 @@ export interface SnippetContentsAdd { language: string; } +export interface SnippetContentsUpdate { + label?: string; + value?: string | null; + language?: string; +} + export interface SnippetsAdd { name: string; folderId?: @@ -29,8 +35,8 @@ export interface SnippetsAdd { } export interface SnippetsUpdate { - name: string; - folderId: + name?: string; + folderId?: | ( | string | ( @@ -39,19 +45,19 @@ export interface SnippetsUpdate { ) ) | null; - description: string | null; + description?: string | null; /** * @min 0 * @max 1 */ - isDeleted: + isDeleted?: | string | (string | (string | (string | (string | (string | (string | number)))))); /** * @min 0 * @max 1 */ - isFavorites: + isFavorites?: | string | (string | (string | (string | (string | (string | (string | number)))))); } @@ -601,17 +607,17 @@ export class Api< * No description * * @tags Snippets - * @name PutSnippetsById - * @request PUT:/snippets/{id} + * @name PatchSnippetsById + * @request PATCH:/snippets/{id} */ - putSnippetsById: ( + patchSnippetsById: ( id: string, data: SnippetsUpdate, params: RequestParams = {}, ) => this.request({ path: `/snippets/${id}`, - method: "PUT", + method: "PATCH", body: data, type: ContentType.Json, ...params, @@ -635,18 +641,18 @@ export class Api< * No description * * @tags Snippets - * @name PutSnippetsByIdContentsByContentId - * @request PUT:/snippets/{id}/contents/{contentId} + * @name PatchSnippetsByIdContentsByContentId + * @request PATCH:/snippets/{id}/contents/{contentId} */ - putSnippetsByIdContentsByContentId: ( + patchSnippetsByIdContentsByContentId: ( id: string, contentId: string, - data: SnippetContentsAdd, + data: SnippetContentsUpdate, params: RequestParams = {}, ) => this.request({ path: `/snippets/${id}/contents/${contentId}`, - method: "PUT", + method: "PATCH", body: data, type: ContentType.Json, ...params, From 1bdb2af0aedffbb6d9f4dfc37aa30adbb8f6bbb7 Mon Sep 17 00:00:00 2001 From: Anton Reshetov Date: Wed, 19 Mar 2025 12:12:00 +0300 Subject: [PATCH 2/2] refactor(api): use PATCH method to update folders --- src/main/api/dto/folders.ts | 12 +- src/main/api/routes/folders.ts | 214 ++++++++++-------- src/renderer/components/sidebar/Sidebar.vue | 32 +-- .../components/sidebar/folders/TreeNode.vue | 9 +- src/renderer/composables/useFolders.ts | 2 +- src/renderer/services/api/generated/index.ts | 20 +- 6 files changed, 151 insertions(+), 138 deletions(-) diff --git a/src/main/api/dto/folders.ts b/src/main/api/dto/folders.ts index 4fe61b99..9f2dca9c 100644 --- a/src/main/api/dto/folders.ts +++ b/src/main/api/dto/folders.ts @@ -5,12 +5,12 @@ const foldersAdd = t.Object({ }) const foldersUpdate = t.Object({ - name: t.String(), - icon: t.Union([t.String(), t.Null()]), - defaultLanguage: t.String(), - parentId: t.Union([t.Number(), t.Null()]), - isOpen: t.Number({ minimum: 0, maximum: 1 }), - orderIndex: t.Number(), + name: t.Optional(t.String()), + icon: t.Optional(t.Union([t.String(), t.Null()])), + defaultLanguage: t.Optional(t.String()), + parentId: t.Optional(t.Union([t.Number(), t.Null()])), + isOpen: t.Optional(t.Number({ minimum: 0, maximum: 1 })), + orderIndex: t.Optional(t.Number()), }) const foldersItem = t.Object({ diff --git a/src/main/api/routes/folders.ts b/src/main/api/routes/folders.ts index db9ca298..d3c6dab5 100644 --- a/src/main/api/routes/folders.ts +++ b/src/main/api/routes/folders.ts @@ -136,119 +136,151 @@ app }, ) // Обновление папки - .put( + .patch( '/:id', ({ params, body, error }) => { - const now = Date.now() const { id } = params - const { name, icon, defaultLanguage, parentId, isOpen, orderIndex } - = body + const now = Date.now() + + const updateFields: string[] = [] + const updateParams: any[] = [] + let needOrderUpdate = false + let newParentId: number | null | undefined + let newOrderIndex: number | undefined + + if ('name' in body) { + updateFields.push('name = ?') + updateParams.push(body.name) + } + + if ('icon' in body) { + updateFields.push('icon = ?') + updateParams.push(body.icon) + } + + if ('defaultLanguage' in body) { + updateFields.push('defaultLanguage = ?') + updateParams.push(body.defaultLanguage) + } + + if ('isOpen' in body) { + updateFields.push('isOpen = ?') + updateParams.push(body.isOpen) + } + + if ('parentId' in body) { + updateFields.push('parentId = ?') + updateParams.push(body.parentId) + newParentId = body.parentId + needOrderUpdate = true + } + + if ('orderIndex' in body) { + updateFields.push('orderIndex = ?') + updateParams.push(body.orderIndex) + newOrderIndex = body.orderIndex + needOrderUpdate = true + } + + if (updateFields.length === 0) { + return error(400, { message: 'Need at least one field to update' }) + } + + updateFields.push('updatedAt = ?') + + updateParams.push(now) + updateParams.push(id) const transaction = db.transaction(() => { + // Получаем текущие данные папки const folder = db - .prepare( - ` - SELECT parentId, orderIndex - FROM folders - WHERE id = ? - `, - ) - .get(id) as { parentId: number | null, orderIndex: number } + .prepare('SELECT parentId, orderIndex FROM folders WHERE id = ?') + .get(id) as + | { parentId: number | null, orderIndex: number } + | undefined if (!folder) { return error(404, { message: 'Folder not found' }) } - // Если изменился родитель или позиция - if (parentId !== folder.parentId || orderIndex !== folder.orderIndex) { - if (parentId === folder.parentId) { - // Перемещение в пределах одного родителя - if (orderIndex > folder.orderIndex) { - // Двигаем вниз - уменьшаем индексы папок между старой и новой позицией + // Обновляем порядок, только если изменился родитель или индекс + if (needOrderUpdate) { + const currentParentId = folder.parentId + const currentOrderIndex = folder.orderIndex + const targetParentId + = newParentId === undefined ? currentParentId : newParentId + const targetOrderIndex + = newOrderIndex === undefined ? currentOrderIndex : newOrderIndex + + if ( + targetParentId !== currentParentId + || targetOrderIndex !== currentOrderIndex + ) { + if (targetParentId === currentParentId) { + // Перемещение в пределах одного родителя + if (targetOrderIndex > currentOrderIndex) { + // Двигаем вниз - уменьшаем индексы папок между старой и новой позицией + db.prepare( + `UPDATE folders + SET orderIndex = orderIndex - 1 + WHERE parentId ${currentParentId === null ? 'IS NULL' : '= ?'} + AND orderIndex > ? + AND orderIndex <= ?`, + ).run( + ...(currentParentId === null + ? [currentOrderIndex, targetOrderIndex] + : [currentParentId, currentOrderIndex, targetOrderIndex]), + ) + } + else { + // Двигаем вверх - увеличиваем индексы папок между новой и старой позицией + db.prepare( + `UPDATE folders + SET orderIndex = orderIndex + 1 + WHERE parentId ${currentParentId === null ? 'IS NULL' : '= ?'} + AND orderIndex >= ? + AND orderIndex < ?`, + ).run( + ...(currentParentId === null + ? [targetOrderIndex, currentOrderIndex] + : [currentParentId, targetOrderIndex, currentOrderIndex]), + ) + } + } + else { + // Перемещение между разными родителями + // 1. Обновляем индексы в старом родителе db.prepare( - ` - UPDATE folders - SET orderIndex = orderIndex - 1 - WHERE parentId ${folder.parentId === null ? 'IS NULL' : '= ?'} - AND orderIndex > ? - AND orderIndex <= ? - `, + `UPDATE folders + SET orderIndex = orderIndex - 1 + WHERE parentId ${currentParentId === null ? 'IS NULL' : '= ?'} + AND orderIndex > ?`, ).run( - ...(folder.parentId === null - ? [folder.orderIndex, orderIndex] - : [folder.parentId, folder.orderIndex, orderIndex]), + ...(currentParentId === null + ? [currentOrderIndex] + : [currentParentId, currentOrderIndex]), ) - } - else { - // Двигаем вверх - увеличиваем индексы папок между новой и старой позицией + + // 2. Обновляем индексы в новом родителе db.prepare( - ` - UPDATE folders - SET orderIndex = orderIndex + 1 - WHERE parentId ${folder.parentId === null ? 'IS NULL' : '= ?'} - AND orderIndex >= ? - AND orderIndex < ? - `, + `UPDATE folders + SET orderIndex = orderIndex + 1 + WHERE parentId ${targetParentId === null ? 'IS NULL' : '= ?'} + AND orderIndex >= ?`, ).run( - ...(folder.parentId === null - ? [orderIndex, folder.orderIndex] - : [folder.parentId, orderIndex, folder.orderIndex]), + ...(targetParentId === null + ? [targetOrderIndex] + : [targetParentId, targetOrderIndex]), ) } } - else { - // Перемещение между разными родителями - // 1. Обновляем индексы в старом родителе - db.prepare( - ` - UPDATE folders - SET orderIndex = orderIndex - 1 - WHERE parentId ${folder.parentId === null ? 'IS NULL' : '= ?'} - AND orderIndex > ? - `, - ).run( - ...(folder.parentId === null - ? [folder.orderIndex] - : [folder.parentId, folder.orderIndex]), - ) - - // 2. Обновляем индексы в новом родителе - db.prepare( - ` - UPDATE folders - SET orderIndex = orderIndex + 1 - WHERE parentId ${parentId === null ? 'IS NULL' : '= ?'} - AND orderIndex >= ? - `, - ).run( - ...(parentId === null ? [orderIndex] : [parentId, orderIndex]), - ) - } } // Обновляем саму папку - db.prepare( - ` - UPDATE folders - SET name = ?, - icon = ?, - defaultLanguage = ?, - isOpen = ?, - parentId = ?, - orderIndex = ?, - updatedAt = ? - WHERE id = ? - `, - ).run( - name, - icon, - defaultLanguage, - isOpen, - parentId, - orderIndex, - now, - id, - ) + const updateStmt = db.prepare(` + UPDATE folders SET ${updateFields.join(', ')} WHERE id = ? + `) + updateStmt.run(...updateParams) }) transaction() diff --git a/src/renderer/components/sidebar/Sidebar.vue b/src/renderer/components/sidebar/Sidebar.vue index 1a94f8d2..b6992146 100644 --- a/src/renderer/components/sidebar/Sidebar.vue +++ b/src/renderer/components/sidebar/Sidebar.vue @@ -4,7 +4,6 @@ import { ScrollArea } from '@/components/ui/shadcn/scroll-area' import { useApp, useFolders, useGutter, useSnippets } from '@/composables' import { LibraryFilter } from '@/composables/types' import { i18n, store } from '@/electron' -import { api } from '@/services/api' import { scrollToElement } from '@/utils' import { Archive, Inbox, Plus, Star, Trash } from 'lucide-vue-next' import { APP_DEFAULTS } from '~/main/store/constants' @@ -16,8 +15,13 @@ const gutterRef = ref<{ $el: HTMLElement }>() const { sidebarWidth, selectedFolderId } = useApp() const { getSnippets, selectFirstSnippet, searchQuery } = useSnippets() -const { getFolders, folders, selectFolder, createFolderAndSelect } - = useFolders() +const { + getFolders, + folders, + selectFolder, + createFolderAndSelect, + updateFolder, +} = useFolders() const { width } = useGutter( sidebarRef, gutterRef, @@ -58,19 +62,9 @@ async function onFolderClick(id: number) { async function onFolderToggle(node: Node) { try { - const { id, name, icon, defaultLanguage, parentId, isOpen, orderIndex } - = node - - await api.folders.putFoldersById(String(id), { - name, - icon, - defaultLanguage, - parentId, - isOpen: !isOpen ? 1 : 0, - orderIndex, - }) + const { id, isOpen } = node - await getFolders() + updateFolder(id, { isOpen: !isOpen ? 1 : 0 }) } catch (error) { console.error('Folder update error:', error) @@ -121,16 +115,10 @@ async function onFolderDrag({ } } - await api.folders.putFoldersById(String(node.id), { - name: node.name, - icon: node.icon || null, - defaultLanguage: node.defaultLanguage || 'plain_text', + updateFolder(node.id, { parentId: newParentId, - isOpen: node.isOpen ? 1 : 0, orderIndex: newOrderIndex, }) - - await getFolders() } catch (error) { console.error('Folder update error:', error) diff --git a/src/renderer/components/sidebar/folders/TreeNode.vue b/src/renderer/components/sidebar/folders/TreeNode.vue index 48fbcbee..ac3a5803 100644 --- a/src/renderer/components/sidebar/folders/TreeNode.vue +++ b/src/renderer/components/sidebar/folders/TreeNode.vue @@ -214,14 +214,7 @@ function onUpdateName() { return } - updateFolder(props.node.id, { - name: name.value, - icon: props.node.icon, - defaultLanguage: props.node.defaultLanguage, - parentId: props.node.parentId, - isOpen: props.node.isOpen, - orderIndex: props.node.orderIndex, - }) + updateFolder(props.node.id, { name: name.value }) renameFolderId.value = null } diff --git a/src/renderer/composables/useFolders.ts b/src/renderer/composables/useFolders.ts index 17f6046e..1cb4d8b3 100644 --- a/src/renderer/composables/useFolders.ts +++ b/src/renderer/composables/useFolders.ts @@ -57,7 +57,7 @@ async function createFolderAndSelect() { async function updateFolder(folderId: number, data: FoldersUpdate) { try { - await api.folders.putFoldersById(String(folderId), data) + await api.folders.patchFoldersById(String(folderId), data) await getFolders() if (folderId === selectedFolderId.value) { diff --git a/src/renderer/services/api/generated/index.ts b/src/renderer/services/api/generated/index.ts index 6cd8bc47..3cc6376d 100644 --- a/src/renderer/services/api/generated/index.ts +++ b/src/renderer/services/api/generated/index.ts @@ -166,20 +166,20 @@ export type FoldersResponse = { }[]; export interface FoldersUpdate { - name: string; - icon: string | null; - defaultLanguage: string; - parentId: + name?: string; + icon?: string | null; + defaultLanguage?: string; + parentId?: | (string | (string | (string | (string | (string | (string | number)))))) | null; /** * @min 0 * @max 1 */ - isOpen: + isOpen?: | string | (string | (string | (string | (string | (string | number))))); - orderIndex: + orderIndex?: | string | (string | (string | (string | (string | (string | number))))); } @@ -733,17 +733,17 @@ export class Api< * No description * * @tags Folders - * @name PutFoldersById - * @request PUT:/folders/{id} + * @name PatchFoldersById + * @request PATCH:/folders/{id} */ - putFoldersById: ( + patchFoldersById: ( id: string, data: FoldersUpdate, params: RequestParams = {}, ) => this.request({ path: `/folders/${id}`, - method: "PUT", + method: "PATCH", body: data, type: ContentType.Json, ...params,