bunigniter 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +229 -0
- package/dist/base/controller.ts +324 -0
- package/dist/base/index.ts +5 -0
- package/dist/base/service.ts +21 -0
- package/dist/cli/index.ts +318 -0
- package/dist/cli/list-routes.ts +72 -0
- package/dist/cli/repl.ts +461 -0
- package/dist/cli/templates.ts +283 -0
- package/dist/client/index.ts +159 -0
- package/dist/db/drizzle.ts +550 -0
- package/dist/db/validators.ts +229 -0
- package/dist/edge-builder.ts +120 -0
- package/dist/edge.ts +69 -0
- package/dist/helpers/cache.ts +173 -0
- package/dist/helpers/cors.ts +103 -0
- package/dist/helpers/csrf.ts +155 -0
- package/dist/helpers/debug.ts +158 -0
- package/dist/helpers/env.ts +147 -0
- package/dist/helpers/handler.ts +158 -0
- package/dist/helpers/http.ts +194 -0
- package/dist/helpers/image.ts +217 -0
- package/dist/helpers/jwt.ts +147 -0
- package/dist/helpers/logger.ts +96 -0
- package/dist/helpers/mail.ts +272 -0
- package/dist/helpers/middleware-loader.ts +116 -0
- package/dist/helpers/middleware.ts +57 -0
- package/dist/helpers/modules.ts +115 -0
- package/dist/helpers/openapi.ts +140 -0
- package/dist/helpers/pagination.ts +159 -0
- package/dist/helpers/queue.ts +186 -0
- package/dist/helpers/request-context.ts +13 -0
- package/dist/helpers/request.ts +376 -0
- package/dist/helpers/schedule.ts +173 -0
- package/dist/helpers/session-middleware.ts +89 -0
- package/dist/helpers/session.ts +286 -0
- package/dist/helpers/sse.ts +90 -0
- package/dist/helpers/throttle.ts +156 -0
- package/dist/helpers/upload.ts +417 -0
- package/dist/helpers/validator.ts +287 -0
- package/dist/helpers/ws.ts +123 -0
- package/dist/index.ts +221 -0
- package/dist/package.json +70 -0
- package/dist/router/file-router.ts +541 -0
- package/dist/router/server-router.ts +103 -0
- package/dist/view/page.ts +96 -0
- package/dist/view/renderer.tsx +390 -0
- package/dist/view/view-response.ts +10 -0
- package/package.json +70 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared template renderer for all `make:*` CLI commands.
|
|
3
|
+
* Single source of truth — update here to affect all generators.
|
|
4
|
+
*/
|
|
5
|
+
export function render(template: string, data: Record<string, any>): string {
|
|
6
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key: string) => String(data[key] ?? ''))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Route controller template. */
|
|
10
|
+
export function controller(name: string, prefix: string): string {
|
|
11
|
+
return render(`/**
|
|
12
|
+
* {{Name}} controller
|
|
13
|
+
*
|
|
14
|
+
* GET {{prefix}}/{{name}}
|
|
15
|
+
* POST {{prefix}}/{{name}}
|
|
16
|
+
* PUT {{prefix}}/{{name}}/:id
|
|
17
|
+
* DELETE {{prefix}}/{{name}}/:id
|
|
18
|
+
*/
|
|
19
|
+
import { Controller } from 'bunigniter'
|
|
20
|
+
|
|
21
|
+
export class {{Name}} extends Controller {
|
|
22
|
+
async index() {
|
|
23
|
+
return this.json({ message: '{{name}} index' })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async show(id: number) {
|
|
27
|
+
return this.json({ message: '{{name}} show', id })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async create() {
|
|
31
|
+
const v = this.validate(this.body, {
|
|
32
|
+
// name: 'required'
|
|
33
|
+
})
|
|
34
|
+
if (v.fails()) return this.badRequest(v.errors)
|
|
35
|
+
return this.json({ message: '{{name}} created' }, 201)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async update(id: number) {
|
|
39
|
+
return this.json({ message: '{{name}} updated', id })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async destroy(id: number) {
|
|
43
|
+
return this.json({ message: '{{name}} deleted', id })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`, { name, Name: name.charAt(0).toUpperCase() + name.slice(1), prefix })
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** DB schema template. */
|
|
50
|
+
export function model(name: string, columns: string): string {
|
|
51
|
+
const tableName = name.toLowerCase() + 's'
|
|
52
|
+
const cols = columns.split(',').filter(Boolean).map((c: string) => {
|
|
53
|
+
const [colName, colType] = c.trim().split(':')
|
|
54
|
+
return { name: colName || 'id', type: colType || 'string' }
|
|
55
|
+
})
|
|
56
|
+
const fieldDefs = cols.map((c: any) => {
|
|
57
|
+
if (c.type === 'number' || c.type === 'integer') {
|
|
58
|
+
return ` ${c.name}: integer('${c.name}'),`
|
|
59
|
+
}
|
|
60
|
+
return ` ${c.name}: text('${c.name}'),`
|
|
61
|
+
}).join('\n')
|
|
62
|
+
|
|
63
|
+
return `import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
|
|
64
|
+
|
|
65
|
+
export const ${tableName} = sqliteTable('${tableName}', {
|
|
66
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
67
|
+
${fieldDefs}
|
|
68
|
+
createdAt: text('created_at').default('CURRENT_TIMESTAMP'),
|
|
69
|
+
})
|
|
70
|
+
`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Migration file template. */
|
|
74
|
+
export function migration(name: string): string {
|
|
75
|
+
const timestamp = Date.now()
|
|
76
|
+
const tableName = name.toLowerCase().replace(/^create_|^add_|^drop_/, '').replace(/_table$/, '') + 's'
|
|
77
|
+
const isCreate = name.toLowerCase().startsWith('create')
|
|
78
|
+
|
|
79
|
+
return `-- Migration: ${name}
|
|
80
|
+
-- Generated at: ${new Date().toISOString()}
|
|
81
|
+
|
|
82
|
+
${isCreate ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
83
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
84
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
85
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
86
|
+
);` : `-- Add your migration SQL here
|
|
87
|
+
-- ALTER TABLE ${tableName} ADD COLUMN ...;`}
|
|
88
|
+
`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Middleware template. */
|
|
92
|
+
export function middleware(name: string): string {
|
|
93
|
+
return render(`/**
|
|
94
|
+
* {{name}} middleware
|
|
95
|
+
*/
|
|
96
|
+
import { defineMiddleware } from 'bunigniter'
|
|
97
|
+
|
|
98
|
+
export default defineMiddleware(async (c, next) => {
|
|
99
|
+
const start = performance.now()
|
|
100
|
+
await next()
|
|
101
|
+
const duration = Math.round((performance.now() - start) * 100) / 100
|
|
102
|
+
c.set.headers ??= {}
|
|
103
|
+
c.set.headers['X-{{Name}}-Time'] = \`\${duration}ms\`
|
|
104
|
+
})
|
|
105
|
+
`, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** CLI command template. */
|
|
109
|
+
export function command(name: string): string {
|
|
110
|
+
return render(`/**
|
|
111
|
+
* {{name}} command — scaffolded CLI command
|
|
112
|
+
*/
|
|
113
|
+
import type { CommandArgs } from '../cli/types'
|
|
114
|
+
|
|
115
|
+
export default {
|
|
116
|
+
name: '{{name}}',
|
|
117
|
+
desc: 'Description for {{name}}',
|
|
118
|
+
async run(args: CommandArgs) {
|
|
119
|
+
console.log('Running {{name}} with args:', args)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
`, { name })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Test file template. */
|
|
126
|
+
export function test(name: string): string {
|
|
127
|
+
return render(`/**
|
|
128
|
+
* {{name}} tests
|
|
129
|
+
*/
|
|
130
|
+
import { describe, it, expect } from 'vitest'
|
|
131
|
+
|
|
132
|
+
describe('{{Name}}', () => {
|
|
133
|
+
it('should work', () => {
|
|
134
|
+
expect(1 + 1).toBe(2)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
`, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Queue job template. */
|
|
141
|
+
export function job(name: string): string {
|
|
142
|
+
return render(`/**
|
|
143
|
+
* {{name}} job — queue worker
|
|
144
|
+
*/
|
|
145
|
+
import type { Job } from 'bunigniter/helpers/queue'
|
|
146
|
+
|
|
147
|
+
export default async function (job: Job) {
|
|
148
|
+
const { data } = job
|
|
149
|
+
console.log('Processing {{name}} job:', data)
|
|
150
|
+
// Add your job logic here
|
|
151
|
+
}
|
|
152
|
+
`, { name })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Mail class template. */
|
|
156
|
+
export function mail(name: string): string {
|
|
157
|
+
return render(`/**
|
|
158
|
+
* {{name}} mail
|
|
159
|
+
*/
|
|
160
|
+
export async function send{{Name}}(to: string, data: Record<string, any> = {}) {
|
|
161
|
+
// const { mail } = await import('bunigniter/helpers/mail')
|
|
162
|
+
// await mail.send({ to, subject: '{{Name}}', html: \`<h1>\${data.title}</h1>\` })
|
|
163
|
+
console.log('Sending {{name}} mail to', to, data)
|
|
164
|
+
}
|
|
165
|
+
`, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Seeder template. */
|
|
169
|
+
export function seeder(name: string): string {
|
|
170
|
+
return render(`/**
|
|
171
|
+
* {{name}} seeder
|
|
172
|
+
*/
|
|
173
|
+
export default async function seed(ctx: any) {
|
|
174
|
+
const { db } = ctx
|
|
175
|
+
// await db.query('INSERT INTO {{name}} (name) VALUES (?)', ['Sample'])
|
|
176
|
+
console.log('Seeding {{name}}...')
|
|
177
|
+
}
|
|
178
|
+
`, { name })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Event template. */
|
|
182
|
+
export function eventTemplate(name: string): string {
|
|
183
|
+
return render(`/**
|
|
184
|
+
* {{name}} event
|
|
185
|
+
*/
|
|
186
|
+
export class {{Name}} {
|
|
187
|
+
constructor(public readonly data: any) {}
|
|
188
|
+
}
|
|
189
|
+
`, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Listener template. */
|
|
193
|
+
export function listener(name: string): string {
|
|
194
|
+
return render(`/**
|
|
195
|
+
* {{name}} listener
|
|
196
|
+
*/
|
|
197
|
+
export default async function handle{{Name}}(event: any) {
|
|
198
|
+
console.log('Handling {{name}}:', event.data)
|
|
199
|
+
}
|
|
200
|
+
`, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Service provider template. */
|
|
204
|
+
export function provider(name: string): string {
|
|
205
|
+
return render(`/**
|
|
206
|
+
* {{name}} service provider
|
|
207
|
+
*/
|
|
208
|
+
export default {
|
|
209
|
+
register() {
|
|
210
|
+
// Register bindings here
|
|
211
|
+
},
|
|
212
|
+
boot() {
|
|
213
|
+
// Run after all providers are registered
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
`, { name })
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Policy template. */
|
|
220
|
+
export function policy(name: string): string {
|
|
221
|
+
return render(`/**
|
|
222
|
+
* {{name}} policy
|
|
223
|
+
*/
|
|
224
|
+
export class {{Name}}Policy {
|
|
225
|
+
view(user: any, resource: any) { return true }
|
|
226
|
+
create(user: any) { return true }
|
|
227
|
+
update(user: any, resource: any) { return user.id === resource.user_id }
|
|
228
|
+
delete(user: any, resource: any) { return user.id === resource.user_id }
|
|
229
|
+
}
|
|
230
|
+
`, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Form request template. */
|
|
234
|
+
export function formRequest(name: string): string {
|
|
235
|
+
return render(`/**
|
|
236
|
+
* {{name}} form request
|
|
237
|
+
*/
|
|
238
|
+
import { z } from 'zod'
|
|
239
|
+
|
|
240
|
+
export const {{Name}}Schema = z.object({
|
|
241
|
+
// name: z.string().min(2),
|
|
242
|
+
// email: z.string().email(),
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
export type {{Name}}Data = z.infer<typeof {{Name}}Schema>
|
|
246
|
+
`, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** API resource template. */
|
|
250
|
+
export function resource(name: string): string {
|
|
251
|
+
return render(`/**
|
|
252
|
+
* {{name}} API resource
|
|
253
|
+
*/
|
|
254
|
+
export interface {{Name}} {
|
|
255
|
+
id: number
|
|
256
|
+
// Add fields here
|
|
257
|
+
createdAt: string
|
|
258
|
+
updatedAt: string
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function {{name}}ToJson(item: {{Name}}): Record<string, any> {
|
|
262
|
+
return {
|
|
263
|
+
id: item.id,
|
|
264
|
+
// Map fields here
|
|
265
|
+
createdAt: item.createdAt,
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
`, { name, Name: name.charAt(0).toUpperCase() + name.slice(1) })
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Validation rule template. */
|
|
272
|
+
export function rule(name: string): string {
|
|
273
|
+
return render(`/**
|
|
274
|
+
* {{name}} validation rule
|
|
275
|
+
*/
|
|
276
|
+
export function {{name}}(value: any, params?: string): string | null {
|
|
277
|
+
if (!value) return null
|
|
278
|
+
// Add validation logic
|
|
279
|
+
// return 'Validation failed'
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
`, { name })
|
|
283
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed Fetch Client — Void-style type-safe API client.
|
|
3
|
+
*
|
|
4
|
+
* Generates a type-safe fetch client from route definitions.
|
|
5
|
+
* In the browser, uses `fetch`. In the server, calls `app.fetch()` directly.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { api } from 'nexusts/client'
|
|
10
|
+
*
|
|
11
|
+
* // GET: fully typed response
|
|
12
|
+
* const users = await api.get('/api/users')
|
|
13
|
+
*
|
|
14
|
+
* // POST: typed body + response
|
|
15
|
+
* const created = await api.post('/api/users', { name: 'Alice', email: 'a@b.com' })
|
|
16
|
+
*
|
|
17
|
+
* // With params
|
|
18
|
+
* const user = await api.get('/api/users/:id', { params: { id: '42' } })
|
|
19
|
+
*
|
|
20
|
+
* // With query
|
|
21
|
+
* const result = await api.get('/api/search', { query: { q: 'hello' } })
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface FetchOptions {
|
|
26
|
+
/** HTTP method. Default: GET */
|
|
27
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
28
|
+
|
|
29
|
+
/** Request body (auto-serialized to JSON). */
|
|
30
|
+
body?: any
|
|
31
|
+
|
|
32
|
+
/** Query parameters. */
|
|
33
|
+
query?: Record<string, string | number | boolean | undefined>
|
|
34
|
+
|
|
35
|
+
/** URL path parameters (e.g. `{ id: '42' }` for /:id). */
|
|
36
|
+
params?: Record<string, string | number>
|
|
37
|
+
|
|
38
|
+
/** Additional headers. */
|
|
39
|
+
headers?: Record<string, string>
|
|
40
|
+
|
|
41
|
+
/** Base URL. Default: '' (same origin). */
|
|
42
|
+
baseURL?: string
|
|
43
|
+
|
|
44
|
+
/** Abort signal. */
|
|
45
|
+
signal?: AbortSignal
|
|
46
|
+
|
|
47
|
+
/** Timeout in ms. */
|
|
48
|
+
timeout?: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface FetchError {
|
|
52
|
+
status: number
|
|
53
|
+
data: any
|
|
54
|
+
response: Response
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Typed fetch helper.
|
|
59
|
+
*
|
|
60
|
+
* @param path - URL path (supports `:param` placeholders)
|
|
61
|
+
* @param options - Request options
|
|
62
|
+
* @returns Parsed response body
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* import { fetch } from 'nexusts/client'
|
|
67
|
+
* const users = await fetch('/api/users')
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export async function fetch<T = any>(path: string, options: FetchOptions = {}): Promise<T> {
|
|
71
|
+
const { method = 'GET', body, query, params, headers: extraHeaders, baseURL = '', signal, timeout } = options
|
|
72
|
+
|
|
73
|
+
// Interpolate path params
|
|
74
|
+
let resolvedPath = path
|
|
75
|
+
if (params) {
|
|
76
|
+
for (const [key, value] of Object.entries(params)) {
|
|
77
|
+
resolvedPath = resolvedPath.replace(`:${key}`, String(value))
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Build URL
|
|
82
|
+
const url = new URL(resolvedPath, baseURL || 'http://localhost')
|
|
83
|
+
if (query) {
|
|
84
|
+
for (const [key, value] of Object.entries(query)) {
|
|
85
|
+
if (value !== undefined && value !== '') {
|
|
86
|
+
url.searchParams.set(key, String(value))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Build request
|
|
92
|
+
const headers: Record<string, string> = {
|
|
93
|
+
...(extraHeaders ?? {}),
|
|
94
|
+
}
|
|
95
|
+
if (body && method !== 'GET') {
|
|
96
|
+
headers['content-type'] = 'application/json'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const requestInit: RequestInit = {
|
|
100
|
+
method,
|
|
101
|
+
headers,
|
|
102
|
+
signal,
|
|
103
|
+
...(body && method !== 'GET' ? { body: JSON.stringify(body) } : {}),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Timeout handling
|
|
107
|
+
let timeoutId: Timer | undefined
|
|
108
|
+
if (timeout) {
|
|
109
|
+
const controller = new AbortController()
|
|
110
|
+
timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
111
|
+
requestInit.signal = controller.signal
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(url.toString().replace(/^http:\/\/localhost/, ''), requestInit)
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const error: FetchError = {
|
|
119
|
+
status: response.status,
|
|
120
|
+
data: await response.json().catch(() => null),
|
|
121
|
+
response,
|
|
122
|
+
}
|
|
123
|
+
throw error
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle empty responses
|
|
127
|
+
const contentType = response.headers.get('content-type') ?? ''
|
|
128
|
+
if (contentType.includes('application/json')) {
|
|
129
|
+
return response.json()
|
|
130
|
+
}
|
|
131
|
+
if (contentType.includes('text/')) {
|
|
132
|
+
return response.text() as unknown as T
|
|
133
|
+
}
|
|
134
|
+
return undefined as unknown as T
|
|
135
|
+
} finally {
|
|
136
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Convenience methods matching Void's fetch pattern.
|
|
142
|
+
*/
|
|
143
|
+
export const api = {
|
|
144
|
+
get<T = any>(path: string, options?: Omit<FetchOptions, 'method'>): Promise<T> {
|
|
145
|
+
return fetch<T>(path, { ...options, method: 'GET' })
|
|
146
|
+
},
|
|
147
|
+
post<T = any>(path: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>): Promise<T> {
|
|
148
|
+
return fetch<T>(path, { ...options, method: 'POST', body })
|
|
149
|
+
},
|
|
150
|
+
put<T = any>(path: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>): Promise<T> {
|
|
151
|
+
return fetch<T>(path, { ...options, method: 'PUT', body })
|
|
152
|
+
},
|
|
153
|
+
delete<T = any>(path: string, options?: Omit<FetchOptions, 'method'>): Promise<T> {
|
|
154
|
+
return fetch<T>(path, { ...options, method: 'DELETE' })
|
|
155
|
+
},
|
|
156
|
+
patch<T = any>(path: string, body?: any, options?: Omit<FetchOptions, 'method' | 'body'>): Promise<T> {
|
|
157
|
+
return fetch<T>(path, { ...options, method: 'PATCH', body })
|
|
158
|
+
},
|
|
159
|
+
}
|