@panomc/client-gen 0.0.0-stage → 1.0.0-dev.61

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.
@@ -0,0 +1,210 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+ import { canonicalJson } from './canonical.js'
5
+ import { generateClient } from './generate.js'
6
+ import { run } from './cli.js'
7
+ import { CLIENT_PKG, FIXTURE, TSC, tmp } from './test-helpers.js'
8
+
9
+ const GOLDEN = new URL('./golden/mini/', import.meta.url).pathname
10
+ const fixtureText = readFileSync(FIXTURE, 'utf8')
11
+ const fixture = JSON.parse(fixtureText)
12
+
13
+ /** @param {any} v @returns {any} same data, object keys in reverse order */
14
+ function reverseKeys(v) {
15
+ if (Array.isArray(v)) return v.map(reverseKeys)
16
+ if (v && typeof v === 'object') return Object.fromEntries(Object.keys(v).reverse().map((k) => [k, reverseKeys(v[k])]))
17
+ return v
18
+ }
19
+
20
+ describe('generate: golden files for the fixture', () => {
21
+ const gen = generateClient(fixtureText)
22
+
23
+ test('the four files match src/golden/mini (UPDATE_GOLDEN=1 rewrites them)', () => {
24
+ if (process.env.UPDATE_GOLDEN) {
25
+ mkdirSync(GOLDEN, { recursive: true })
26
+ for (const [name, content] of Object.entries(gen.files)) writeFileSync(join(GOLDEN, name), content)
27
+ }
28
+ expect(Object.keys(gen.files).sort()).toEqual(['index.js', 'openapi.json', 'operations.js', 'types.js'])
29
+ expect(readdirSync(GOLDEN).sort()).toEqual(['index.js', 'openapi.json', 'operations.js', 'types.js'])
30
+ for (const [name, content] of Object.entries(gen.files)) expect(content).toBe(readFileSync(join(GOLDEN, name), 'utf8'))
31
+ })
32
+
33
+ test('deterministic: a second run and a reordered document give the same bytes', () => {
34
+ expect(generateClient(fixtureText).files).toEqual(gen.files)
35
+ expect(generateClient(reverseKeys(fixture)).files).toEqual(gen.files)
36
+ expect(gen.files['openapi.json']).toBe(canonicalJson(fixture))
37
+ expect(Object.values(gen.files).join('')).not.toMatch(/\d{4}-\d{2}-\d{2}T/)
38
+ })
39
+
40
+ test('mapping of doc 06 2.3', () => {
41
+ const { 'index.js': index, 'types.js': types, 'operations.js': ops } = gen.files
42
+ // function name = operationId with a lower-cased first letter, one function per operation
43
+ expect(index).toContain('export const getPosts = (client, params) => client.request(operations.getPosts, params);')
44
+ expect(ops).toContain("getPosts: { method: 'GET', path: '/api/v1/posts', auth: 'none', mutation: false }")
45
+ expect(ops).toContain("createPost: { method: 'POST', path: '/api/v1/posts', auth: 'user', mutation: true }")
46
+ // optional query -> optional params; sorted query keys
47
+ expect(index).toContain('{ query?: { category?: string, page?: number, pageSize?: number }, signal?: AbortSignal }} [params]')
48
+ // required body -> required params
49
+ expect(index).toContain('{ body: import(\'./types.js\').CreatePostRequest, signal?: AbortSignal }} params')
50
+ // deprecated
51
+ expect(index).toMatch(/@deprecated\n \* @param \{import\('@panomc\/client'\)\.PanoClient\} client\n \* @param \{\{ body: import/)
52
+ // multipart -> FormData, binary -> Blob, no content -> void
53
+ expect(index).toContain('body?: FormData')
54
+ expect(types).toContain('/** @typedef {Blob} GetApiV1ExportByKindResponse */')
55
+ expect(types).toContain('/** @typedef {void} DeletePostResponse */')
56
+ // object -> @typedef {Object} + @property, optional = [name]
57
+ expect(types).toContain(' * @typedef {Object} Author')
58
+ expect(types).toContain(' * @property {string | null} [avatar] Head image url.')
59
+ expect(types).toContain(' * @property {number} id')
60
+ // enum -> literal union, nullable -> |null, oneOf -> union, $ref -> name, arrays, additionalProperties
61
+ expect(types).toContain("status: 'DRAFT' | 'PUBLISHED'")
62
+ expect(types).toContain('image?: string | null')
63
+ expect(types).toContain('body?: string | Author')
64
+ expect(types).toContain('author: Author')
65
+ expect(types).toContain('tags?: string[]')
66
+ expect(types).toContain('meta?: Record<string, string>')
67
+ expect(types).toContain("'x-extra'?: boolean")
68
+ expect(types).toContain('/** @typedef {Author & { extra?: number }} Combined */')
69
+ expect(types).toContain("/** @typedef {'UP' | 'DOWN'} Status */")
70
+ expect(types.trimEnd().endsWith('export {};')).toBe(true)
71
+ // x-pano-undocumented -> any
72
+ expect(types).toContain('/** @typedef {any} GetLegacyThingResponse */')
73
+ expect(index).toContain('{ query?: Record<string, any>, signal?: AbortSignal }} [params]')
74
+ })
75
+
76
+ test('operation without operationId: <method><PascalPath> and a warning', () => {
77
+ expect(gen.warnings).toEqual(['GET /api/v1/export/{kind} has no operationId, using "getApiV1ExportByKind"'])
78
+ expect(gen.files['operations.js']).toContain('getApiV1ExportByKind:')
79
+ })
80
+
81
+ test('a path parameter makes params required and typed', () => {
82
+ expect(gen.files['index.js']).toContain('{ path: { url: string }, signal?: AbortSignal }} params')
83
+ })
84
+ })
85
+
86
+ describe('generate: input errors', () => {
87
+ test('not JSON / not OpenAPI', () => {
88
+ expect(() => generateClient('{nope')).toThrow('The OpenAPI document is not valid JSON')
89
+ expect(() => generateClient({ openapi: '3.1.0' })).toThrow('no "paths" object')
90
+ })
91
+
92
+ test('colliding names get a suffix and a warning, unknown refs become any', () => {
93
+ const gen = generateClient({
94
+ openapi: '3.1.0',
95
+ paths: {
96
+ '/a': { get: { operationId: 'Dup', responses: { 200: { content: { 'application/json': { schema: { $ref: '#/components/schemas/Nope' } } } } } } },
97
+ '/b': { get: { operationId: 'Dup', responses: { 200: { description: 'x' } } } },
98
+ '/c': { get: { operationId: 'delete', responses: {} } }
99
+ }
100
+ })
101
+ expect(gen.files['operations.js']).toContain('dup:')
102
+ expect(gen.files['operations.js']).toContain('dup2:')
103
+ expect(gen.files['operations.js']).toContain('deleteOperation:')
104
+ expect(gen.warnings.some((w) => w.includes('unknown schema "Nope"'))).toBe(true)
105
+ })
106
+ })
107
+
108
+ describe('generate: the output type checks', () => {
109
+ /** @param {string[]} args @param {string} cwd */
110
+ function tsc(args, cwd) {
111
+ const r = Bun.spawnSync(['bun', TSC, ...args], { cwd })
112
+ return (r.stdout.toString() + r.stderr.toString()).trim()
113
+ }
114
+
115
+ /** @param {string} dir */
116
+ function layout(dir) {
117
+ mkdirSync(join(dir, 'node_modules', '@panomc'), { recursive: true })
118
+ symlinkSync(CLIENT_PKG, join(dir, 'node_modules', '@panomc', 'client'))
119
+ const gen = generateClient(fixtureText)
120
+ mkdirSync(join(dir, 'src', 'lib', 'pano', 'core'), { recursive: true })
121
+ for (const [name, content] of Object.entries(gen.files)) writeFileSync(join(dir, 'src', 'lib', 'pano', 'core', name), content)
122
+ }
123
+
124
+ const flags = ['--noEmit', '--allowJs', '--checkJs', '--strict', '--module', 'esnext', '--moduleResolution', 'bundler', '--target', 'es2022', '--lib', 'es2022,dom', '--skipLibCheck']
125
+
126
+ test('tsc --checkJs over the generated files', () => {
127
+ const dir = tmp('tsc-out')
128
+ try {
129
+ layout(dir)
130
+ const out = tsc([...flags, ...['index.js', 'types.js', 'operations.js'].map((f) => `src/lib/pano/core/${f}`)], dir)
131
+ // errors inside @panomc/client itself are not the generator's; only the generated folder counts
132
+ const mine = out.split('\n').filter((l) => l.startsWith('src/'))
133
+ expect(mine).toEqual([])
134
+ } finally {
135
+ rmSync(dir, { recursive: true, force: true })
136
+ }
137
+ })
138
+
139
+ test('a consumer with a plain jsconfig.json type checks, and a wrong call is rejected', () => {
140
+ const dir = tmp('tsc-consumer')
141
+ try {
142
+ layout(dir)
143
+ writeFileSync(
144
+ join(dir, 'jsconfig.json'),
145
+ JSON.stringify({
146
+ compilerOptions: { checkJs: true, allowJs: true, noEmit: true, strict: true, module: 'esnext', moduleResolution: 'bundler', target: 'es2022', lib: ['es2022', 'dom'], skipLibCheck: true },
147
+ include: ['src']
148
+ })
149
+ )
150
+ mkdirSync(join(dir, 'src', 'routes'), { recursive: true })
151
+ writeFileSync(
152
+ join(dir, 'src', 'routes', 'ok.js'),
153
+ `import { createClient } from '@panomc/client'
154
+ import { getPosts, getPost, createPost, uploadAvatar, deletePost } from '../lib/pano/core/index.js'
155
+ import { operations } from '../lib/pano/core/operations.js'
156
+
157
+ /** @typedef {import('../lib/pano/core/types.js').Post} Post */
158
+
159
+ const client = createClient({ baseUrl: 'http://localhost:8088', csrf: 'off' })
160
+
161
+ export async function load() {
162
+ const res = await getPosts(client, { query: { page: 1, category: 'news' } })
163
+ if (!res.ok) return { code: res.error.code }
164
+ /** @type {Post} */
165
+ const first = res.data.items[0]
166
+ const title = first.title.toUpperCase()
167
+ const one = await getPost(client, { path: { url: 'hello' } })
168
+ const created = await createPost(client, { body: { title: 'x' } })
169
+ await uploadAvatar(client, { body: new FormData() })
170
+ await deletePost(client, { path: { url: 'a' } })
171
+ return { title, one, created, method: operations.getPosts.method }
172
+ }
173
+ `
174
+ )
175
+ expect(tsc(['-p', 'jsconfig.json'], dir).split('\n').filter((l) => l.startsWith('src/'))).toEqual([])
176
+
177
+ writeFileSync(
178
+ join(dir, 'src', 'routes', 'bad.js'),
179
+ `import { createClient } from '@panomc/client'
180
+ import { getPost } from '../lib/pano/core/index.js'
181
+ const client = createClient({ baseUrl: 'http://localhost:8088' })
182
+ export const r = getPost(client, {})
183
+ export const q = getPost(client, { path: { url: 5 } })
184
+ `
185
+ )
186
+ const bad = tsc(['-p', 'jsconfig.json'], dir)
187
+ expect(bad).toContain('src/routes/bad.js')
188
+ } finally {
189
+ rmSync(dir, { recursive: true, force: true })
190
+ }
191
+ })
192
+ })
193
+
194
+ describe('generate command', () => {
195
+ test('writes the four files and reports the warning on stderr', async () => {
196
+ const dir = tmp('cmd')
197
+ try {
198
+ const errs = []
199
+ const outs = []
200
+ const code = await run(['generate', '--input', FIXTURE, '--out', join(dir, 'o')], { stdout: (s) => outs.push(s), stderr: (s) => errs.push(s) })
201
+ expect(code).toBe(0)
202
+ expect(readdirSync(join(dir, 'o')).sort()).toEqual(['index.js', 'openapi.json', 'operations.js', 'types.js'])
203
+ expect(errs[0]).toContain('warning: GET /api/v1/export/{kind} has no operationId')
204
+ expect(outs[0]).toContain('generated 7 operations')
205
+ expect(await run(['generate'], { stderr: (s) => errs.push(s) })).toBe(1)
206
+ } finally {
207
+ rmSync(dir, { recursive: true, force: true })
208
+ }
209
+ })
210
+ })
@@ -0,0 +1,59 @@
1
+ // Generated by pano-client from openapi.json. Do not edit; run pano-client again.
2
+ import { operations } from './operations.js';
3
+
4
+ /**
5
+ * Create a post.
6
+ * @deprecated
7
+ * @param {import('@panomc/client').PanoClient} client
8
+ * @param {{ body: import('./types.js').CreatePostRequest, signal?: AbortSignal }} params
9
+ * @returns {Promise<import('@panomc/client').ApiResult<import('./types.js').CreatePostResponse>>}
10
+ */
11
+ export const createPost = (client, params) => client.request(operations.createPost, params);
12
+
13
+ /**
14
+ * DELETE /api/v1/posts/{url}
15
+ * @param {import('@panomc/client').PanoClient} client
16
+ * @param {{ path: { url: string }, signal?: AbortSignal }} params
17
+ * @returns {Promise<import('@panomc/client').ApiResult<import('./types.js').DeletePostResponse>>}
18
+ */
19
+ export const deletePost = (client, params) => client.request(operations.deletePost, params);
20
+
21
+ /**
22
+ * Download an export.
23
+ * @param {import('@panomc/client').PanoClient} client
24
+ * @param {{ path: { kind: 'posts' | 'users' }, signal?: AbortSignal }} params
25
+ * @returns {Promise<import('@panomc/client').ApiResult<import('./types.js').GetApiV1ExportByKindResponse>>}
26
+ */
27
+ export const getApiV1ExportByKind = (client, params) => client.request(operations.getApiV1ExportByKind, params);
28
+
29
+ /**
30
+ * GET /api/v1/legacy/thing
31
+ * @param {import('@panomc/client').PanoClient} client
32
+ * @param {{ query?: Record<string, any>, signal?: AbortSignal }} [params]
33
+ * @returns {Promise<import('@panomc/client').ApiResult<import('./types.js').GetLegacyThingResponse>>}
34
+ */
35
+ export const getLegacyThing = (client, params) => client.request(operations.getLegacyThing, params);
36
+
37
+ /**
38
+ * One post by its url.
39
+ * @param {import('@panomc/client').PanoClient} client
40
+ * @param {{ path: { url: string }, signal?: AbortSignal }} params
41
+ * @returns {Promise<import('@panomc/client').ApiResult<import('./types.js').GetPostResponse>>}
42
+ */
43
+ export const getPost = (client, params) => client.request(operations.getPost, params);
44
+
45
+ /**
46
+ * List published posts.
47
+ * @param {import('@panomc/client').PanoClient} client
48
+ * @param {{ query?: { category?: string, page?: number, pageSize?: number }, signal?: AbortSignal }} [params]
49
+ * @returns {Promise<import('@panomc/client').ApiResult<import('./types.js').GetPostsResponse>>}
50
+ */
51
+ export const getPosts = (client, params) => client.request(operations.getPosts, params);
52
+
53
+ /**
54
+ * POST /api/v1/profile/avatar
55
+ * @param {import('@panomc/client').PanoClient} client
56
+ * @param {{ body?: FormData, signal?: AbortSignal }} [params]
57
+ * @returns {Promise<import('@panomc/client').ApiResult<import('./types.js').UploadAvatarResponse>>}
58
+ */
59
+ export const uploadAvatar = (client, params) => client.request(operations.uploadAvatar, params);
@@ -0,0 +1,412 @@
1
+ {
2
+ "components": {
3
+ "schemas": {
4
+ "Author": {
5
+ "description": "Who wrote a post.",
6
+ "properties": {
7
+ "avatar": {
8
+ "description": "Head image url.",
9
+ "type": [
10
+ "string",
11
+ "null"
12
+ ]
13
+ },
14
+ "id": {
15
+ "type": "integer"
16
+ },
17
+ "username": {
18
+ "type": "string"
19
+ }
20
+ },
21
+ "required": [
22
+ "id",
23
+ "username"
24
+ ],
25
+ "type": "object"
26
+ },
27
+ "Combined": {
28
+ "allOf": [
29
+ {
30
+ "$ref": "#/components/schemas/Author"
31
+ },
32
+ {
33
+ "properties": {
34
+ "extra": {
35
+ "type": "number"
36
+ }
37
+ },
38
+ "type": "object"
39
+ }
40
+ ]
41
+ },
42
+ "ErrorEnvelope": {
43
+ "properties": {
44
+ "error": {
45
+ "properties": {
46
+ "code": {
47
+ "type": "string"
48
+ },
49
+ "fields": {
50
+ "additionalProperties": {
51
+ "type": "string"
52
+ },
53
+ "type": "object"
54
+ },
55
+ "message": {
56
+ "type": "string"
57
+ }
58
+ },
59
+ "required": [
60
+ "code"
61
+ ],
62
+ "type": "object"
63
+ }
64
+ },
65
+ "required": [
66
+ "error"
67
+ ],
68
+ "type": "object"
69
+ },
70
+ "PageInfo": {
71
+ "properties": {
72
+ "number": {
73
+ "type": "integer"
74
+ },
75
+ "size": {
76
+ "type": "integer"
77
+ },
78
+ "totalItems": {
79
+ "type": "integer"
80
+ },
81
+ "totalPages": {
82
+ "type": "integer"
83
+ }
84
+ },
85
+ "required": [
86
+ "number",
87
+ "size",
88
+ "totalItems",
89
+ "totalPages"
90
+ ],
91
+ "type": "object"
92
+ },
93
+ "Post": {
94
+ "properties": {
95
+ "author": {
96
+ "$ref": "#/components/schemas/Author"
97
+ },
98
+ "body": {
99
+ "oneOf": [
100
+ {
101
+ "type": "string"
102
+ },
103
+ {
104
+ "$ref": "#/components/schemas/Author"
105
+ }
106
+ ]
107
+ },
108
+ "id": {
109
+ "type": "integer"
110
+ },
111
+ "image": {
112
+ "nullable": true,
113
+ "type": "string"
114
+ },
115
+ "meta": {
116
+ "additionalProperties": {
117
+ "type": "string"
118
+ },
119
+ "type": "object"
120
+ },
121
+ "status": {
122
+ "enum": [
123
+ "DRAFT",
124
+ "PUBLISHED"
125
+ ],
126
+ "type": "string"
127
+ },
128
+ "tags": {
129
+ "items": {
130
+ "type": "string"
131
+ },
132
+ "type": "array"
133
+ },
134
+ "title": {
135
+ "type": "string"
136
+ },
137
+ "x-extra": {
138
+ "type": "boolean"
139
+ }
140
+ },
141
+ "required": [
142
+ "id",
143
+ "title",
144
+ "status",
145
+ "author"
146
+ ],
147
+ "type": "object"
148
+ },
149
+ "PostInput": {
150
+ "properties": {
151
+ "categoryId": {
152
+ "type": "integer"
153
+ },
154
+ "text": {
155
+ "type": "string"
156
+ },
157
+ "title": {
158
+ "type": "string"
159
+ }
160
+ },
161
+ "required": [
162
+ "title"
163
+ ],
164
+ "type": "object"
165
+ },
166
+ "PostPage": {
167
+ "properties": {
168
+ "items": {
169
+ "items": {
170
+ "$ref": "#/components/schemas/Post"
171
+ },
172
+ "type": "array"
173
+ },
174
+ "page": {
175
+ "$ref": "#/components/schemas/PageInfo"
176
+ }
177
+ },
178
+ "required": [
179
+ "items",
180
+ "page"
181
+ ],
182
+ "type": "object"
183
+ },
184
+ "Status": {
185
+ "enum": [
186
+ "UP",
187
+ "DOWN"
188
+ ],
189
+ "type": "string"
190
+ }
191
+ }
192
+ },
193
+ "info": {
194
+ "title": "Pano mini",
195
+ "version": "1.0.0"
196
+ },
197
+ "openapi": "3.1.0",
198
+ "paths": {
199
+ "/api/v1/export/{kind}": {
200
+ "get": {
201
+ "parameters": [
202
+ {
203
+ "in": "path",
204
+ "name": "kind",
205
+ "required": true,
206
+ "schema": {
207
+ "enum": [
208
+ "posts",
209
+ "users"
210
+ ],
211
+ "type": "string"
212
+ }
213
+ }
214
+ ],
215
+ "responses": {
216
+ "200": {
217
+ "content": {
218
+ "application/octet-stream": {
219
+ "schema": {
220
+ "format": "binary",
221
+ "type": "string"
222
+ }
223
+ }
224
+ },
225
+ "description": "file"
226
+ }
227
+ },
228
+ "summary": "Download an export."
229
+ }
230
+ },
231
+ "/api/v1/legacy/thing": {
232
+ "get": {
233
+ "operationId": "GetLegacyThing",
234
+ "responses": {
235
+ "200": {
236
+ "description": "ok"
237
+ }
238
+ },
239
+ "x-pano-undocumented": true
240
+ }
241
+ },
242
+ "/api/v1/posts": {
243
+ "get": {
244
+ "operationId": "GetPosts",
245
+ "parameters": [
246
+ {
247
+ "in": "query",
248
+ "name": "page",
249
+ "schema": {
250
+ "type": "integer"
251
+ }
252
+ },
253
+ {
254
+ "in": "query",
255
+ "name": "pageSize",
256
+ "schema": {
257
+ "type": "integer"
258
+ }
259
+ },
260
+ {
261
+ "in": "query",
262
+ "name": "category",
263
+ "schema": {
264
+ "type": "string"
265
+ }
266
+ }
267
+ ],
268
+ "responses": {
269
+ "200": {
270
+ "content": {
271
+ "application/json": {
272
+ "schema": {
273
+ "$ref": "#/components/schemas/PostPage"
274
+ }
275
+ }
276
+ },
277
+ "description": "ok"
278
+ }
279
+ },
280
+ "summary": "List published posts."
281
+ },
282
+ "post": {
283
+ "deprecated": true,
284
+ "operationId": "CreatePost",
285
+ "requestBody": {
286
+ "content": {
287
+ "application/json": {
288
+ "schema": {
289
+ "$ref": "#/components/schemas/PostInput"
290
+ }
291
+ }
292
+ },
293
+ "required": true
294
+ },
295
+ "responses": {
296
+ "201": {
297
+ "content": {
298
+ "application/json": {
299
+ "schema": {
300
+ "$ref": "#/components/schemas/Post"
301
+ }
302
+ }
303
+ },
304
+ "description": "created"
305
+ },
306
+ "400": {
307
+ "content": {
308
+ "application/json": {
309
+ "schema": {
310
+ "$ref": "#/components/schemas/ErrorEnvelope"
311
+ }
312
+ }
313
+ },
314
+ "description": "invalid"
315
+ }
316
+ },
317
+ "security": [
318
+ {
319
+ "cookieAuth": []
320
+ }
321
+ ],
322
+ "summary": "Create a post."
323
+ }
324
+ },
325
+ "/api/v1/posts/{url}": {
326
+ "delete": {
327
+ "operationId": "DeletePost",
328
+ "responses": {
329
+ "204": {
330
+ "description": "deleted"
331
+ }
332
+ },
333
+ "security": [
334
+ {
335
+ "cookieAuth": []
336
+ }
337
+ ]
338
+ },
339
+ "get": {
340
+ "operationId": "GetPost",
341
+ "responses": {
342
+ "200": {
343
+ "content": {
344
+ "application/json": {
345
+ "schema": {
346
+ "$ref": "#/components/schemas/Post"
347
+ }
348
+ }
349
+ },
350
+ "description": "ok"
351
+ }
352
+ },
353
+ "summary": "One post by its url."
354
+ },
355
+ "parameters": [
356
+ {
357
+ "in": "path",
358
+ "name": "url",
359
+ "required": true,
360
+ "schema": {
361
+ "type": "string"
362
+ }
363
+ }
364
+ ]
365
+ },
366
+ "/api/v1/profile/avatar": {
367
+ "post": {
368
+ "operationId": "UploadAvatar",
369
+ "requestBody": {
370
+ "content": {
371
+ "multipart/form-data": {
372
+ "schema": {
373
+ "properties": {
374
+ "file": {
375
+ "format": "binary",
376
+ "type": "string"
377
+ }
378
+ },
379
+ "type": "object"
380
+ }
381
+ }
382
+ }
383
+ },
384
+ "responses": {
385
+ "200": {
386
+ "content": {
387
+ "application/json": {
388
+ "schema": {
389
+ "properties": {
390
+ "url": {
391
+ "type": "string"
392
+ }
393
+ },
394
+ "required": [
395
+ "url"
396
+ ],
397
+ "type": "object"
398
+ }
399
+ }
400
+ },
401
+ "description": "ok"
402
+ }
403
+ },
404
+ "security": [
405
+ {
406
+ "cookieAuth": []
407
+ }
408
+ ]
409
+ }
410
+ }
411
+ }
412
+ }
@@ -0,0 +1,11 @@
1
+ // Generated by pano-client from openapi.json. Do not edit; run pano-client again.
2
+
3
+ export const operations = /** @type {const} */ ({
4
+ createPost: { method: 'POST', path: '/api/v1/posts', auth: 'user', mutation: true },
5
+ deletePost: { method: 'DELETE', path: '/api/v1/posts/{url}', auth: 'user', mutation: true },
6
+ getApiV1ExportByKind: { method: 'GET', path: '/api/v1/export/{kind}', auth: 'none', mutation: false },
7
+ getLegacyThing: { method: 'GET', path: '/api/v1/legacy/thing', auth: 'none', mutation: false },
8
+ getPost: { method: 'GET', path: '/api/v1/posts/{url}', auth: 'none', mutation: false },
9
+ getPosts: { method: 'GET', path: '/api/v1/posts', auth: 'none', mutation: false },
10
+ uploadAvatar: { method: 'POST', path: '/api/v1/profile/avatar', auth: 'user', mutation: true }
11
+ });