diff --git a/AGENTS.md b/AGENTS.md index 1f03e28..d2c4665 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,22 +12,26 @@ HTTP API, протокол HTTP, Postman, js-playwright. Уроки и само ## Как собран сервер Четыре спецификации на TypeSpec (`typespec//`) компилируются в OpenAPI -(`make compile` → `tsp-output/`), и каждую отдаёт свой мок prism. Рядом живёт -приложение на fastify (`custom-server/`) с рукописными эндпоинтами: `/http-api/rpc`, -`/http-api/echo`, стрим и куки для курса про протокол, статика, а также REST-маршруты -коллекций `/http-api/tasks`, `/users`, `/posts`, `/comments` — их забрали у -статичного мока: тот не умеет ни пагинации, ни выборки полей, ни отбора по пути. -Снаружи всё сшивает Caddy на `$PORT`. +(`make compile` → `tsp-output/`), по одной на курс: `http-api`, `http-protocol`, +`js-playwright`, `postman`. Каждую поднимает свой prism, и за ним остались только +`/courses` и `/login`. Всё остальное обслуживает приложение на fastify +(`custom-server/`): коллекции `tasks`, `users`, `posts`, `comments` по каждому +префиксу плюс рукописные эндпоинты `/http-api/rpc`, `/http-api/echo`, +`/http-protocol/login`, стрим и куки для курса про протокол, статика. Снаружи всё +сшивает Caddy на `$PORT`. ```text Caddy :$PORT ├── :4010 fastify коллекции, rpc, echo, стрим, куки, статика, swagger-ui -├── :4011 prism http-api: только /courses и /login -├── :4012 prism http-protocol -├── :4013 prism js-playwright -└── :4014 prism postman +├── :4011 prism http-api: только /courses и /login +├── :4012 prism http-protocol: только /courses +├── :4013 prism js-playwright: только /courses и /login +└── :4014 prism postman: только /courses и /login ``` +У `http-protocol` логин рукописный и отвечает строкой `Done!`: курс про протокол +разбирает его через telnet и печатает в уроке `Content-Length: 5`. + Список приложений и путь к документации задаёт `app.config.json`: имя приложения плюс `docRoute` дают маршрут вида `/http-api-openapi`. @@ -38,26 +42,33 @@ prism получает `/tasks`. Директива `handle /http-api/rpc` пр эндпоинт поверх мока это блок `handle` более специфичный, чем общий `handle_path`. -## Мок `http-api` статичный, остальные три динамические +## Мок статичный у всех четырёх, и данных почти не отдаёт + +Ни у одной спецификации в `bin/start.sh` нет флага `-d`. Динамический режим +генерировал данные faker'ом, а тот не смотрит на объявленный тип: при `uint16` +(0..65535) приходило `"id":-39177245`, ответы менялись на каждый запрос, и +опираться на них уроки не могли. Статичный режим отдаёт примеры из спецификации +(`@example` у моделей), поэтому у `Course`, `Courses` и `AuthToken` они заведены +во всех четырёх — это всё, что мок ещё обслуживает. -В `bin/start.sh` у `http-api` **нет** флага `-d`, у остальных трёх он есть. Это -намеренная разница. +Коллекции мок не обслуживает вовсе: они реализованы в `custom-server/` общим +кодом (`routes.js`, `collections.js`, таблица в `resources.js`). В статичном +режиме пример отдаётся дословно, поэтому `skip`, `limit` и `select` не +применялись, а на любой `/users/{id}` приходила одна и та же запись. +Спецификации коллекции по-прежнему описывают, и по ним читают документацию, но +поведение задаёт код. -В статичном режиме prism отдаёт примеры из спецификации (`@example` у моделей). -В динамическом генерирует данные faker'ом: тот не смотрит на объявленный тип, -поэтому при `uint16` (0..65535) выдавал `"id":-39177245`, а ответы менялись на -каждый запрос, и опираться на них уроки не могли. +Различия между спецификациями настоящие. Они сняты с `typespec//services/` +и выписаны в таблице `SPECS`: -Три остальные спецификации остаются динамическими, потому что примеров в них нет: -статичный режим отдал бы вместо данных заглушки вида `"string"`. Чтобы перевести -любую из них на статику, сначала заводят `@example` во всех её моделях. +* задачи курса Postman закрыты **Basic**, причём включая чтение одной задачи; +* у `js-playwright` есть только задачи и пользователи, всё открыто, вложенных + ресурсов нет; +* остальное закрыто Bearer по тому же рисунку, что у `http-api`. -Маршруты `/http-api/tasks` не обслуживает ни один режим мока: они реализованы в -`custom-server/src/tasks-rest.js`. В статичном режиме пример отдаётся дословно, -поэтому `skip` и `limit` не применялись, а на любой `/tasks/{id}` приходила одна -и та же задача. -Спецификация их по-прежнему описывает, и по ней читают документацию, но -поведение задаёт код. +Создание везде отвечает **201**. Три спецификации объявляли обычный ответ, то +есть 200, и урок `850-api-testing` курса Playwright из-за этого не проходил, хотя +учил правильному коду. Спецификации выровнены по `http-api`, а не урок по ним. ## Примеры ответов это текст уроков @@ -120,10 +131,17 @@ prism получает `/tasks`. Директива `handle /http-api/rpc` пр ## Прогон -`make test` поднимает статичный мок с приложением и проверяет ответы запросами: -совпадение данных REST и RPC, коды из таблицы выше, границы `uint16`. Нужна +`make test` поднимает **пять** сервисов, четыре мока и приложение, и проверяет +ответы запросами: пагинацию и `select` по каждому префиксу, отбор по пути и +вложенные ресурсы, коды из таблицы выше, схемы авторизации, границы `uint16`, +неизменяемость наборов и совпадение примеров спецификации с данными. Нужна скомпилированная спецификация, то есть сначала `make setup` либо `make compile`. +Пять сервисов на загруженной машине поднимаются долго, поэтому ожидание +готовности щедрое. Если сервис не поднялся, прогон печатает его вывод. Пустой +вывод у всех сразу означает не поломку кода, а нехватку ресурсов: стоит +посмотреть `uptime` и снять лишние контейнеры. + Caddy в прогоне не участвует, в раннере его нет: порты опрашиваются напрямую, как это делает Caddy со срезанным префиксом. Сборка гоняется и на пуллреквестах, публикация образа — только с `main`. @@ -158,13 +176,21 @@ make deploy APP=http_example HOST=timeweb SKIP=caddy,cron * у автора 1 восемь постов — урок показывает вложенный ресурс `/users/1/posts`, и на пустом списке он ничего не объясняет; * первые три пользователя и первые два поста автора 1 приведены в уроке - дословно. + дословно; +* пользователей больше пяти — урок `200-complex-requests` курса Postman учит на + `?skip=3&limit=5`. + +Наборы **общие на все четыре префикса**: курсы не зависят от того, чтобы у каждого +были свои записи, а одна копия не расходится сама с собой. + +Данные держатся **на латинице**. Сервер один на все локали курсов, и русский +текст в ответах читался бы как ошибка у испанского или английского студента. ## Набор данных не меняется -`POST` и `DELETE` отвечают 201 и 204, но список задач остаётся прежним: создание -и удаление только сообщают, что произошло бы. Сервер учебный, запросы к нему идут -одновременно от множества студентов, и мутации сделали бы уроки +`POST` и `DELETE` отвечают 201 и 204, но наборы остаются прежними: создание, +обновление и удаление только сообщают, что произошло бы. Сервер учебный, запросы +к нему идут одновременно от множества студентов, и мутации сделали бы уроки невоспроизводимыми: самостоятельная описывает один набор, а следующий студент получил бы другой. Прогон `make test` проверяет, что после `POST` набор задач остался прежним. diff --git a/Caddyfile b/Caddyfile index 30c6122..cd0e7d6 100644 --- a/Caddyfile +++ b/Caddyfile @@ -60,6 +60,39 @@ handle_path /http-api/* { } # http-protocol course +# Коллекции обслуживает приложение, а не мок: см. комментарий у /http-api. +# За моком остаются только /login и /courses. +handle /http-protocol/tasks { + reverse_proxy 127.0.0.1:4010 +} + +handle /http-protocol/tasks/* { + reverse_proxy 127.0.0.1:4010 +} + +handle /http-protocol/users { + reverse_proxy 127.0.0.1:4010 +} + +handle /http-protocol/users/* { + reverse_proxy 127.0.0.1:4010 +} + +handle /http-protocol/posts { + reverse_proxy 127.0.0.1:4010 +} + +handle /http-protocol/posts/* { + reverse_proxy 127.0.0.1:4010 +} + +handle /http-protocol/comments { + reverse_proxy 127.0.0.1:4010 +} + +handle /http-protocol/comments/* { + reverse_proxy 127.0.0.1:4010 +} handle /http-protocol/example { reverse_proxy 127.0.0.1:4010 } @@ -89,6 +122,23 @@ handle_path /http-protocol/* { } # js-playwright course +# Коллекции обслуживает приложение, а не мок: см. комментарий у /http-api. +# За моком остаются только /login и /courses. +handle /js-playwright/tasks { + reverse_proxy 127.0.0.1:4010 +} + +handle /js-playwright/tasks/* { + reverse_proxy 127.0.0.1:4010 +} + +handle /js-playwright/users { + reverse_proxy 127.0.0.1:4010 +} + +handle /js-playwright/users/* { + reverse_proxy 127.0.0.1:4010 +} handle /js-playwright/users-list { reverse_proxy 127.0.0.1:4010 } @@ -102,6 +152,39 @@ handle_path /js-playwright/* { } # postman course +# Коллекции обслуживает приложение, а не мок: см. комментарий у /http-api. +# За моком остаются только /login и /courses. +handle /postman/tasks { + reverse_proxy 127.0.0.1:4010 +} + +handle /postman/tasks/* { + reverse_proxy 127.0.0.1:4010 +} + +handle /postman/users { + reverse_proxy 127.0.0.1:4010 +} + +handle /postman/users/* { + reverse_proxy 127.0.0.1:4010 +} + +handle /postman/posts { + reverse_proxy 127.0.0.1:4010 +} + +handle /postman/posts/* { + reverse_proxy 127.0.0.1:4010 +} + +handle /postman/comments { + reverse_proxy 127.0.0.1:4010 +} + +handle /postman/comments/* { + reverse_proxy 127.0.0.1:4010 +} handle /postman/cookie { reverse_proxy 127.0.0.1:4010 } diff --git a/bin/smoke-test.js b/bin/smoke-test.js index a6b083f..99841e9 100755 --- a/bin/smoke-test.js +++ b/bin/smoke-test.js @@ -34,6 +34,13 @@ import expectedTasks from '../custom-server/src/data/tasks.js'; import expectedUsers from '../custom-server/src/data/users.js'; const SPEC = './tsp-output/http-api/@typespec/openapi3/openapi.1.0.yaml'; +// Порт на приложение задан в package.json, порты моков в bin/start.sh. +const MOCKS = [ + ['http-api', 4011], + ['http-protocol', 4012], + ['js-playwright', 4013], + ['postman', 4014], +]; const PRISM = 'http://127.0.0.1:4011'; const APP = 'http://127.0.0.1:4010'; const TASKS = `${APP}/http-api/tasks`; @@ -94,8 +101,11 @@ const stopAll = async () => { })); }; +// Прогон поднимает пять сервисов: четыре мока и приложение. На загруженной машине +// это заметно дольше одного, поэтому ожидание щедрое. Если сервис не поднялся, +// печатается его вывод, и причина видна сразу. const waitFor = async (url, name) => { - for (let i = 0; i < 60; i += 1) { + for (let i = 0; i < 240; i += 1) { try { await fetch(url); return; @@ -104,7 +114,7 @@ const waitFor = async (url, name) => { } } dumpOutput(); - throw new Error(`${name} не поднялся за 30 секунд: ${url}`); + throw new Error(`${name} не поднялся за 120 секунд: ${url}`); }; const getJson = async (url, options) => { @@ -142,16 +152,21 @@ const outOfRange = (value, path = '$') => { }; const run = async () => { - start('prism', 'npx', [ - 'prism', 'mock', '--multiprocess=false', - '--json-schema-faker-fillProperties=false', - '-p', '4011', '--host', '127.0.0.1', SPEC, - ]); + for (const [app, port] of MOCKS) { + start(`prism-${app}`, 'npx', [ + 'prism', 'mock', '--multiprocess=false', + '--json-schema-faker-fillProperties=false', + '-p', String(port), '--host', '127.0.0.1', + `./tsp-output/${app}/@typespec/openapi3/openapi.1.0.yaml`, + ]); + } start('app', 'npx', [ 'fastify', 'start', '-p', '4010', '-a', '127.0.0.1', 'custom-server/src/index.js', ]); - await waitFor(`${PRISM}/posts`, 'prism'); + for (const [app, port] of MOCKS) { + await waitFor(`http://127.0.0.1:${port}/courses`, `мок ${app}`); + } await waitFor(`${APP}/`, 'приложение'); console.log('\nREST и RPC отдают одни и те же задачи'); @@ -402,6 +417,153 @@ const run = async () => { JSON.stringify(login.body), ); + console.log('\nОстальные три спецификации: те же коллекции тем же кодом'); + // Ожидания выписаны здесь заново, а не взяты из resources.js: прогон должен + // утверждать поведение спецификаций, а не повторять реализацию. Матрица снята + // с typespec//services/. + const OTHER_SPECS = [ + { + prefix: '/http-protocol', collections: ['tasks', 'users', 'posts', 'comments'], nested: true, + guarded: [['DELETE', '/users/1', 'bearer'], ['POST', '/posts', 'bearer']], + open: [['GET', '/tasks/1'], ['POST', '/tasks']], + }, + { + prefix: '/js-playwright', collections: ['tasks', 'users'], nested: false, + guarded: [], + open: [['GET', '/tasks/1'], ['POST', '/users'], ['DELETE', '/users/1']], + }, + { + prefix: '/postman', collections: ['tasks', 'users', 'posts', 'comments'], nested: true, + // Задачи здесь закрыты Basic, и чтение одной задачи тоже. + guarded: [['GET', '/tasks/1', 'basic'], ['POST', '/tasks', 'basic'], ['DELETE', '/users/1', 'bearer']], + open: [['GET', '/tasks'], ['POST', '/users']], + }, + ]; + + const bodyFor = (path) => { + if (path.startsWith('/tasks')) return { title: 'title', description: 'description' }; + if (path.startsWith('/users')) return { + email: 'john@mail.com', firstName: 'John', lastName: 'Doe', password: 'secret', + }; + if (path.startsWith('/posts')) return { title: 'title', body: 'body' }; + return { postId: 1, body: 'body' }; + }; + + const credentials = { bearer: 'Bearer any-value', basic: `Basic ${Buffer.from('user:pass').toString('base64')}` }; + + for (const spec of OTHER_SPECS) { + const at = (path) => `${APP}${spec.prefix}${path}`; + const send = (method, path, scheme) => { + const headers = {}; + if (scheme) headers.Authorization = credentials[scheme]; + if (method === 'POST' || method === 'PATCH') headers['Content-Type'] = 'application/json'; + return getJson(at(path), { + method, + headers, + body: method === 'POST' || method === 'PATCH' ? JSON.stringify(bodyFor(path)) : undefined, + }); + }; + + // Пагинация и отбор по пути — то, чего не умел мок. + const page = await send('GET', '/users?skip=3&limit=2'); + check( + `${spec.prefix}: skip=3&limit=2 применяются`, + JSON.stringify(page.body?.users) === JSON.stringify(expectedUsers.slice(3, 5)) + && page.body?.total === expectedUsers.length, + JSON.stringify(page.body), + ); + const selected = await send('GET', '/users?select=firstName'); + check( + `${spec.prefix}: select оставляет id и запрошенное поле`, + JSON.stringify(Object.keys(selected.body?.users?.[0] ?? {})) === JSON.stringify(['id', 'firstName']), + JSON.stringify(selected.body?.users?.[0]), + ); + const third = await send('GET', '/users/3'); + check( + `${spec.prefix}: /users/3 отдаёт третьего пользователя`, + JSON.stringify(third.body) === JSON.stringify(expectedUsers[2]), + JSON.stringify(third.body), + ); + const missing = await send('GET', '/users/999'); + check(`${spec.prefix}: /users/999 → 404`, missing.status === 404, `получено ${missing.status}`); + const wrongMethod = await send('DELETE', '/users'); + check(`${spec.prefix}: DELETE по коллекции → 405`, wrongMethod.status === 405, `получено ${wrongMethod.status}`); + + // Код создания: у этих трёх спецификаций нет CreatedResponse, значит 200. + const createScheme = spec.guarded.find(([m, p]) => m === 'POST' && p === '/users')?.[2]; + const createdUser = await send('POST', '/users', createScheme); + check( + `${spec.prefix}: POST /users → 201`, + createdUser.status === 201, + `получено ${createdUser.status}`, + ); + + // Состав коллекций: чего в спецификации нет, того быть не должно. + for (const name of ['tasks', 'users', 'posts', 'comments']) { + const { status } = await send('GET', `/${name}`); + const declared = spec.collections.includes(name); + check( + `${spec.prefix}/${name} ${declared ? 'отвечает 200' : 'не объявлена'}`, + declared ? status === 200 : status !== 200, + `получено ${status}`, + ); + } + + if (spec.nested) { + const own = await send('GET', '/users/1/posts'); + check( + `${spec.prefix}: /users/1/posts отбирает по автору`, + JSON.stringify(own.body?.posts) === JSON.stringify(expectedPosts.filter((post) => post.authorId === 1)), + `постов ${own.body?.posts?.length}`, + ); + } + + for (const [method, path, scheme] of spec.guarded) { + const without = await send(method, path); + check(`${spec.prefix}: ${method} ${path} без ${scheme} → 401`, without.status === 401, `получено ${without.status}`); + const withCreds = await send(method, path, scheme); + check( + `${spec.prefix}: ${method} ${path} с ${scheme} проходит`, + withCreds.status < 400, + `получено ${withCreds.status}`, + ); + } + + for (const [method, path] of spec.open) { + const { status } = await send(method, path); + check(`${spec.prefix}: ${method} ${path} открыт`, status < 400, `получено ${status}`); + } + } + + console.log('\nМок отдаёт /courses и /login без латыни по всем префиксам'); + for (const prefix of ['/http-api', '/http-protocol', '/js-playwright', '/postman']) { + // Каддй направляет эти два адреса моку, всё остальное приложению. Мок теперь + // статичный у всех четырёх спецификаций, значит отдаёт примеры, а не faker. + const port = { '/http-api': 4011, '/http-protocol': 4012, '/js-playwright': 4013, '/postman': 4014 }[prefix]; + const mock = `http://127.0.0.1:${port}`; + const courses = await getJson(`${mock}/courses`, { headers: { 'X-API-KEY': 'any-value' } }); + check( + `${prefix}: /courses отдаёт осмысленные курсы`, + courses.body?.courses?.[0]?.title === 'HTTP API' && courses.body?.total === 3, + JSON.stringify(courses.body).slice(0, 120), + ); + check( + `${prefix}: /courses в границах uint16`, + outOfRange(courses.body, `${prefix}/courses`).length === 0, + outOfRange(courses.body, `${prefix}/courses`).join(', '), + ); + const login = await getJson(`${mock}/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: 'max@hotmail.com', password: 'password' }), + }); + check( + `${prefix}: /login отдаёт длинный токен, а не латынь`, + typeof login.body?.token === 'string' && login.body.token.length > 40, + JSON.stringify(login.body), + ); + } + console.log('\nЧисла в ответах не выходят за uint16'); for (const url of [TASKS, `${TASKS}/1`, USERS, `${USERS}/1`, POSTS, `${POSTS}/1`, COMMENTS]) { const path = url.replace(APP, ''); diff --git a/bin/start.sh b/bin/start.sh index 7c5d296..931833a 100755 --- a/bin/start.sh +++ b/bin/start.sh @@ -28,12 +28,12 @@ trap 'stop_all' INT TERM # uint16 и отдавал отрицательные id, а данные при каждом запросе были новые, # из-за чего уроки курса http-api не могли на них опираться. Коды 404, 405, 422 # и 401 статичный режим сохраняет, на них построен урок kinds. -# Остальные три спецификации остаются на -d осознанно: примеров в них нет, и -# статичный режим отдал бы вместо данных заглушки вида "string". +# Все четыре спецификации идут статичным моком. Из них к моку теперь доходят +# только /courses и /login: коллекции обслуживает приложение, см. Caddyfile. start_service prism-http-api "npx prism mock --multiprocess=false --json-schema-faker-fillProperties=false -p 4011 --host 0.0.0.0 ./tsp-output/http-api/@typespec/openapi3/openapi.1.0.yaml" -start_service prism-http-protocol "npx prism mock --multiprocess=false -d --json-schema-faker-fillProperties=false -p 4012 --host 0.0.0.0 ./tsp-output/http-protocol/@typespec/openapi3/openapi.1.0.yaml" -start_service prism-js-playwright "npx prism mock --multiprocess=false -d --json-schema-faker-fillProperties=false -p 4013 --host 0.0.0.0 ./tsp-output/js-playwright/@typespec/openapi3/openapi.1.0.yaml" -start_service prism-postman "npx prism mock --multiprocess=false -d --json-schema-faker-fillProperties=false -p 4014 --host 0.0.0.0 ./tsp-output/postman/@typespec/openapi3/openapi.1.0.yaml" +start_service prism-http-protocol "npx prism mock --multiprocess=false --json-schema-faker-fillProperties=false -p 4012 --host 0.0.0.0 ./tsp-output/http-protocol/@typespec/openapi3/openapi.1.0.yaml" +start_service prism-js-playwright "npx prism mock --multiprocess=false --json-schema-faker-fillProperties=false -p 4013 --host 0.0.0.0 ./tsp-output/js-playwright/@typespec/openapi3/openapi.1.0.yaml" +start_service prism-postman "npx prism mock --multiprocess=false --json-schema-faker-fillProperties=false -p 4014 --host 0.0.0.0 ./tsp-output/postman/@typespec/openapi3/openapi.1.0.yaml" start_service app "npm start" start_service caddy "caddy run" diff --git a/custom-server/src/resources.js b/custom-server/src/resources.js index 6d4c3c4..19d60df 100644 --- a/custom-server/src/resources.js +++ b/custom-server/src/resources.js @@ -1,8 +1,21 @@ -// Описание коллекций: данные, проверки и требования к авторизации. +// Описание коллекций: данные, проверки и требования спецификаций. // -// Требования к авторизации взяты из спецификации: @useAuth(BearerAuth) стоит у -// создания, обновления и удаления постов и комментариев и у обновления с -// удалением пользователей. У задач авторизации нет. +// Все четыре спецификации демонстрационного сервера (http-api, http-protocol, +// js-playwright, postman) объявляют почти одни и те же коллекции, поэтому +// маршруты для них собираются одним кодом. Наборы данных тоже общие: курсы не +// зависят от того, чтобы у каждого префикса были свои записи. +// +// Различия между спецификациями настоящие, и они выписаны в таблице SPECS ниже, +// а не угаданы. Их два: +// +// 1. Схема авторизации. Почти везде Bearer, но задачи курса Postman закрыты +// Basic, причём включая чтение одной задачи. +// 2. Состав. У js-playwright есть только задачи и пользователи, и там всё +// открыто; вложенных ресурсов у него нет. +// +// Создание везде отвечает 201: три спецификации объявляли обычный ответ, то есть +// 200, и урок api-testing курса Playwright из-за этого не проходил, хотя учил +// правильному коду. Спецификации выровнены по http-api. // // Наборы данных не меняются. Сервер учебный, запросы к нему идут одновременно от // множества студентов, и мутации сделали бы уроки невоспроизводимыми: @@ -18,23 +31,22 @@ import comments from './data/comments.js'; import posts from './data/posts.js'; import users from './data/users.js'; +const BEARER = 'bearer'; +const BASIC = 'basic'; + // Автор создаваемой записи не приходит в теле: сервер узнаёт его по токену. // Урок authentication обращает на это внимание отдельно, показывая, что в ответе // появилось поле authorId, которого в запросе не было. const TOKEN_USER_ID = 1; -export default (app) => { - registerCollection(app, { - base: '/http-api/tasks', - envelope: 'tasks', +// Описание одной коллекции, общее для всех префиксов. +const COLLECTIONS = { + tasks: { items: taskStore.items, validate: taskStore.validate, build: taskStore.build, - }); - - registerCollection(app, { - base: '/http-api/users', - envelope: 'users', + }, + users: { items: () => users, validate: (dto, options = {}) => validateFields(dto, { required: ['email', 'firstName', 'lastName', 'password'], @@ -47,12 +59,8 @@ export default (app) => { firstName: dto.firstName, lastName: dto.lastName, }), - auth: { update: true, remove: true }, - }); - - registerCollection(app, { - base: '/http-api/posts', - envelope: 'posts', + }, + posts: { items: () => posts, validate: (dto, options = {}) => validateFields(dto, { required: ['title', 'body'], @@ -64,18 +72,13 @@ export default (app) => { title: dto.title, body: dto.body, }), - auth: { create: true, update: true, remove: true }, - }); - - registerCollection(app, { - base: '/http-api/comments', - envelope: 'comments', + }, + comments: { items: () => comments, validate: (dto, options = {}) => { const problems = validateFields(dto, { required: ['body'], ...options }); - const needsPostId = options.partial !== true; if (dto.postId === undefined) { - if (needsPostId) problems.push('postId обязательно'); + if (options.partial !== true) problems.push('postId обязательно'); } else if (!Number.isInteger(Number(dto.postId))) { problems.push('postId это целое число'); } @@ -87,30 +90,80 @@ export default (app) => { postId: Number(dto.postId), body: dto.body, }), - auth: { create: true, update: true, remove: true }, - }); + }, +}; - registerNested(app, { - base: '/http-api/users', - envelope: 'posts', - parents: () => users, - children: () => posts, - foreignKey: 'authorId', - }); +const NESTED = [ + { parent: 'users', child: 'posts', foreignKey: 'authorId' }, + { parent: 'users', child: 'comments', foreignKey: 'authorId' }, + { parent: 'posts', child: 'comments', foreignKey: 'postId' }, +]; - registerNested(app, { - base: '/http-api/users', - envelope: 'comments', - parents: () => users, - children: () => comments, - foreignKey: 'authorId', - }); +// Таблица снята со спецификаций в typespec//services/. Значение это схема +// авторизации операции, отсутствие ключа означает открытую операцию. +const SPECS = [ + { + prefix: '/http-api', + collections: { + tasks: {}, + users: { update: BEARER, remove: BEARER }, + posts: { create: BEARER, update: BEARER, remove: BEARER }, + comments: { create: BEARER, update: BEARER, remove: BEARER }, + }, + nested: NESTED, + }, + { + prefix: '/http-protocol', + collections: { + tasks: {}, + users: { update: BEARER, remove: BEARER }, + posts: { create: BEARER, update: BEARER, remove: BEARER }, + comments: { create: BEARER, update: BEARER, remove: BEARER }, + }, + nested: NESTED, + }, + { + prefix: '/js-playwright', + collections: { + tasks: {}, + users: {}, + }, + nested: [], + }, + { + prefix: '/postman', + collections: { + // Задачи здесь закрыты Basic, и чтение одной задачи тоже. + tasks: { + get: BASIC, create: BASIC, update: BASIC, remove: BASIC, + }, + users: { update: BEARER, remove: BEARER }, + posts: { create: BEARER, update: BEARER, remove: BEARER }, + comments: { create: BEARER, update: BEARER, remove: BEARER }, + }, + nested: NESTED, + }, +]; + +export default (app) => { + for (const spec of SPECS) { + for (const [name, auth] of Object.entries(spec.collections)) { + registerCollection(app, { + base: `${spec.prefix}/${name}`, + envelope: name, + auth, + ...COLLECTIONS[name], + }); + } - registerNested(app, { - base: '/http-api/posts', - envelope: 'comments', - parents: () => posts, - children: () => comments, - foreignKey: 'postId', - }); + for (const { parent, child, foreignKey } of spec.nested) { + registerNested(app, { + base: `${spec.prefix}/${parent}`, + envelope: child, + parents: COLLECTIONS[parent].items, + children: COLLECTIONS[child].items, + foreignKey, + }); + } + } }; diff --git a/custom-server/src/routes.js b/custom-server/src/routes.js index da5b2c6..99fbe1b 100644 --- a/custom-server/src/routes.js +++ b/custom-server/src/routes.js @@ -28,24 +28,29 @@ const methodNotAllowed = (res, allow) => res }); // Сервер демонстрационный и значение токена не проверяет, важно только наличие -// заголовка. Настоящий сервис здесь сверил бы подпись и срок. Урок +// заголовка нужной схемы. Настоящий сервис сверил бы подпись, срок и пароль. Урок // authentication построен на том, что без заголовка приходит 401. -const hasBearer = (req) => { +// +// Схем две, потому что спецификации разных курсов закрывают маршруты по-разному: +// Bearer почти везде, Basic у задач курса Postman. Схема отражается в заголовке +// WWW-Authenticate, по нему клиент и понимает, что предъявлять. +const SCHEMES = { + bearer: { header: 'Bearer', detail: 'Нужен заголовок Authorization с Bearer-токеном' }, + basic: { header: 'Basic', detail: 'Нужен заголовок Authorization со схемой Basic' }, +}; + +const hasScheme = (req, scheme) => { const header = req.headers.authorization; - return typeof header === 'string' && /^Bearer\s+\S/i.test(header); + return typeof header === 'string' && new RegExp(`^${scheme}\\s+\\S`, 'i').test(header); }; -const unauthorized = (res) => res +const unauthorized = (res, scheme) => res .code(401) - .header('WWW-Authenticate', 'Bearer') - .send({ - title: 'Unauthorized', - status: 401, - detail: 'Нужен заголовок Authorization с Bearer-токеном', - }); + .header('WWW-Authenticate', SCHEMES[scheme].header) + .send({ title: 'Unauthorized', status: 401, detail: SCHEMES[scheme].detail }); -const withAuth = (needsAuth, handler) => (req, res) => { - if (needsAuth && !hasBearer(req)) return unauthorized(res); +const withAuth = (scheme, handler) => (req, res) => { + if (scheme && !hasScheme(req, SCHEMES[scheme].header)) return unauthorized(res, scheme); return handler(req, res); }; @@ -70,11 +75,11 @@ export const registerCollection = (app, { return res.send(page({ items: items(), envelope, ...range, fields })); }); - app.get(item, (req, res) => { + app.get(item, withAuth(auth.get, (req, res) => { const found = findById(items(), req.params.id); if (!found) return notFound(res, req.params.id); return res.send(project(found, parseSelect(req.query.select))); - }); + })); app.post(base, withAuth(auth.create, (req, res) => { const problems = validate(req.body ?? {}); diff --git a/typespec/http-protocol/models/auth.tsp b/typespec/http-protocol/models/auth.tsp index c1b89d0..6b2e614 100644 --- a/typespec/http-protocol/models/auth.tsp +++ b/typespec/http-protocol/models/auth.tsp @@ -6,6 +6,10 @@ model AuthData { password: string; } +// Этот же токен приведён примером в уроках, где показывают ответ /login. +@example(#{ + token: "r4AR4Fo0j29s9mFk4IUVA2rGTQmIrHWlioifaJLSQQYHbTXHxtSLFUVp8PANrRoAb7fgkSsbN7lt4a86pcJ07ivUpxBLyyCHaY4Pp9I7hRPphCHM7xpZ1om1", +}) model AuthToken { @minLength(1) token: string; diff --git a/typespec/http-protocol/models/course.tsp b/typespec/http-protocol/models/course.tsp index 1b576fb..49199c9 100644 --- a/typespec/http-protocol/models/course.tsp +++ b/typespec/http-protocol/models/course.tsp @@ -15,6 +15,11 @@ model EditCourseDto { description?: string; } +@example(#{ + id: 1, + title: "HTTP API", + description: "Designing and calling APIs over HTTP", +}) model Course { @key id: uint16; @@ -27,6 +32,16 @@ model Course { } +@example(#{ + courses: #[ + #{ id: 1, title: "HTTP API", description: "Designing and calling APIs over HTTP" }, + #{ id: 2, title: "The HTTP protocol", description: "Requests, responses, status codes and headers" }, + #{ id: 3, title: "JavaScript basics", description: "A first programming language from scratch" } + ], + total: 3, + skip: 0, + limit: 30, +}) model Courses { courses: Course[]; total: uint16; diff --git a/typespec/http-protocol/services/commentsService.tsp b/typespec/http-protocol/services/commentsService.tsp index 6c3101c..5f1d011 100644 --- a/typespec/http-protocol/services/commentsService.tsp +++ b/typespec/http-protocol/services/commentsService.tsp @@ -1,3 +1,6 @@ +// Эти маршруты обслуживает приложение, а не мок prism, см. custom-server/src/resources.js. +// Спецификация описывает настоящее поведение: skip, limit и select применяются, +// а запрос несуществующей записи отвечает 404. import "@typespec/http"; import "@typespec/rest"; import "../models/comment.tsp"; @@ -23,17 +26,17 @@ interface CommentService { @path id: string, @query select?: string - ): Comment; + ): Comment | NotFoundResponse; @useAuth(BearerAuth) @post - op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewCommentDto): Comment; + op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewCommentDto): CreatedResponse & Comment; @useAuth(BearerAuth) @patch - op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditCommentDto): Comment; + op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditCommentDto): Comment | NotFoundResponse; @useAuth(BearerAuth) @delete - op delete(@path id: string): void; + op delete(@path id: string): NoContentResponse | NotFoundResponse; } diff --git a/typespec/http-protocol/services/postsService.tsp b/typespec/http-protocol/services/postsService.tsp index 4ef969e..766cefa 100644 --- a/typespec/http-protocol/services/postsService.tsp +++ b/typespec/http-protocol/services/postsService.tsp @@ -1,3 +1,6 @@ +// Эти маршруты обслуживает приложение, а не мок prism, см. custom-server/src/resources.js. +// Спецификация описывает настоящее поведение: skip, limit и select применяются, +// а запрос несуществующей записи отвечает 404. import "@typespec/http"; import "@typespec/rest"; import "../models/post.tsp"; @@ -22,19 +25,19 @@ interface PostService { op get( @path id: string, @query select?: string - ): Post; + ): Post | NotFoundResponse; @useAuth(BearerAuth) @post - op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewPostDto): Post; + op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewPostDto): CreatedResponse & Post; @useAuth(BearerAuth) @patch - op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditPostDto): Post; + op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditPostDto): Post | NotFoundResponse; @useAuth(BearerAuth) @delete - op delete(@path id: string): void; + op delete(@path id: string): NoContentResponse | NotFoundResponse; @route("/{postId}/comments") op getComments( diff --git a/typespec/http-protocol/services/tasksService.tsp b/typespec/http-protocol/services/tasksService.tsp index 9a35b0a..950290d 100644 --- a/typespec/http-protocol/services/tasksService.tsp +++ b/typespec/http-protocol/services/tasksService.tsp @@ -1,3 +1,6 @@ +// Эти маршруты обслуживает приложение, а не мок prism, см. custom-server/src/resources.js. +// Спецификация описывает настоящее поведение: skip, limit и select применяются, +// а запрос несуществующей записи отвечает 404. import "@typespec/http"; import "@typespec/rest"; import "../models/task.tsp"; @@ -13,8 +16,8 @@ namespace AppService; interface TaskService { @get op list( - @query skip?: string, - @query limit?: string, + @query skip?: uint16 = 0, + @query limit?: uint16 = 30, @query select?: string[] ): Tasks; @@ -22,14 +25,14 @@ interface TaskService { op get( @path id: string, @query select?: string - ): Task; + ): Task | NotFoundResponse; @post - op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewTaskDto): Task; + op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewTaskDto): CreatedResponse & Task; @patch - op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditTaskDto): Task; + op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditTaskDto): Task | NotFoundResponse; @delete - op delete(@path id: string): void; + op delete(@path id: string): NoContentResponse | NotFoundResponse; } diff --git a/typespec/http-protocol/services/usersService.tsp b/typespec/http-protocol/services/usersService.tsp index c2e23cd..a02323b 100644 --- a/typespec/http-protocol/services/usersService.tsp +++ b/typespec/http-protocol/services/usersService.tsp @@ -1,3 +1,6 @@ +// Эти маршруты обслуживает приложение, а не мок prism, см. custom-server/src/resources.js. +// Спецификация описывает настоящее поведение: skip, limit и select применяются, +// а запрос несуществующей записи отвечает 404. import "@typespec/http"; import "@typespec/rest"; import "../models/user.tsp"; @@ -16,8 +19,8 @@ namespace AppService; interface UserService { @get op list( - @query skip?: string, - @query limit?: string, + @query skip?: uint16 = 0, + @query limit?: uint16 = 30, @query select?: string[] ): Users; @@ -25,18 +28,18 @@ interface UserService { op get( @path id: string, @query select?: string - ): User; + ): User | NotFoundResponse; @post - op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewUserDto): User; + op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewUserDto): CreatedResponse & User; @useAuth(BearerAuth) @patch - op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditUserDto): User; + op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditUserDto): User | NotFoundResponse; @useAuth(BearerAuth) @delete - op delete(@path id: string): void; + op delete(@path id: string): NoContentResponse | NotFoundResponse; @route("/{authorId}/posts") op getPosts( diff --git a/typespec/js-playwright/models/auth.tsp b/typespec/js-playwright/models/auth.tsp index c1b89d0..6b2e614 100644 --- a/typespec/js-playwright/models/auth.tsp +++ b/typespec/js-playwright/models/auth.tsp @@ -6,6 +6,10 @@ model AuthData { password: string; } +// Этот же токен приведён примером в уроках, где показывают ответ /login. +@example(#{ + token: "r4AR4Fo0j29s9mFk4IUVA2rGTQmIrHWlioifaJLSQQYHbTXHxtSLFUVp8PANrRoAb7fgkSsbN7lt4a86pcJ07ivUpxBLyyCHaY4Pp9I7hRPphCHM7xpZ1om1", +}) model AuthToken { @minLength(1) token: string; diff --git a/typespec/js-playwright/models/course.tsp b/typespec/js-playwright/models/course.tsp index 1b576fb..49199c9 100644 --- a/typespec/js-playwright/models/course.tsp +++ b/typespec/js-playwright/models/course.tsp @@ -15,6 +15,11 @@ model EditCourseDto { description?: string; } +@example(#{ + id: 1, + title: "HTTP API", + description: "Designing and calling APIs over HTTP", +}) model Course { @key id: uint16; @@ -27,6 +32,16 @@ model Course { } +@example(#{ + courses: #[ + #{ id: 1, title: "HTTP API", description: "Designing and calling APIs over HTTP" }, + #{ id: 2, title: "The HTTP protocol", description: "Requests, responses, status codes and headers" }, + #{ id: 3, title: "JavaScript basics", description: "A first programming language from scratch" } + ], + total: 3, + skip: 0, + limit: 30, +}) model Courses { courses: Course[]; total: uint16; diff --git a/typespec/js-playwright/services/tasksService.tsp b/typespec/js-playwright/services/tasksService.tsp index 9a35b0a..950290d 100644 --- a/typespec/js-playwright/services/tasksService.tsp +++ b/typespec/js-playwright/services/tasksService.tsp @@ -1,3 +1,6 @@ +// Эти маршруты обслуживает приложение, а не мок prism, см. custom-server/src/resources.js. +// Спецификация описывает настоящее поведение: skip, limit и select применяются, +// а запрос несуществующей записи отвечает 404. import "@typespec/http"; import "@typespec/rest"; import "../models/task.tsp"; @@ -13,8 +16,8 @@ namespace AppService; interface TaskService { @get op list( - @query skip?: string, - @query limit?: string, + @query skip?: uint16 = 0, + @query limit?: uint16 = 30, @query select?: string[] ): Tasks; @@ -22,14 +25,14 @@ interface TaskService { op get( @path id: string, @query select?: string - ): Task; + ): Task | NotFoundResponse; @post - op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewTaskDto): Task; + op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewTaskDto): CreatedResponse & Task; @patch - op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditTaskDto): Task; + op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditTaskDto): Task | NotFoundResponse; @delete - op delete(@path id: string): void; + op delete(@path id: string): NoContentResponse | NotFoundResponse; } diff --git a/typespec/js-playwright/services/usersService.tsp b/typespec/js-playwright/services/usersService.tsp index fcb7fe7..96e2578 100644 --- a/typespec/js-playwright/services/usersService.tsp +++ b/typespec/js-playwright/services/usersService.tsp @@ -1,3 +1,6 @@ +// Эти маршруты обслуживает приложение, а не мок prism, см. custom-server/src/resources.js. +// Спецификация описывает настоящее поведение: skip, limit и select применяются, +// а запрос несуществующей записи отвечает 404. import "@typespec/http"; import "@typespec/rest"; import "../models/user.tsp"; @@ -14,8 +17,8 @@ namespace AppService; interface UserService { @get op list( - @query skip?: string, - @query limit?: string, + @query skip?: uint16 = 0, + @query limit?: uint16 = 30, @query select?: string[] ): Users; @@ -23,14 +26,14 @@ interface UserService { op get( @path id: string, @query select?: string - ): User; + ): User | NotFoundResponse; @post - op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewUserDto): User; + op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewUserDto): CreatedResponse & User; @patch - op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditUserDto): User; + op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditUserDto): User | NotFoundResponse; @delete - op delete(@path id: string): void; + op delete(@path id: string): NoContentResponse | NotFoundResponse; } diff --git a/typespec/postman/models/auth.tsp b/typespec/postman/models/auth.tsp index c1b89d0..6b2e614 100644 --- a/typespec/postman/models/auth.tsp +++ b/typespec/postman/models/auth.tsp @@ -6,6 +6,10 @@ model AuthData { password: string; } +// Этот же токен приведён примером в уроках, где показывают ответ /login. +@example(#{ + token: "r4AR4Fo0j29s9mFk4IUVA2rGTQmIrHWlioifaJLSQQYHbTXHxtSLFUVp8PANrRoAb7fgkSsbN7lt4a86pcJ07ivUpxBLyyCHaY4Pp9I7hRPphCHM7xpZ1om1", +}) model AuthToken { @minLength(1) token: string; diff --git a/typespec/postman/models/course.tsp b/typespec/postman/models/course.tsp index 1b576fb..49199c9 100644 --- a/typespec/postman/models/course.tsp +++ b/typespec/postman/models/course.tsp @@ -15,6 +15,11 @@ model EditCourseDto { description?: string; } +@example(#{ + id: 1, + title: "HTTP API", + description: "Designing and calling APIs over HTTP", +}) model Course { @key id: uint16; @@ -27,6 +32,16 @@ model Course { } +@example(#{ + courses: #[ + #{ id: 1, title: "HTTP API", description: "Designing and calling APIs over HTTP" }, + #{ id: 2, title: "The HTTP protocol", description: "Requests, responses, status codes and headers" }, + #{ id: 3, title: "JavaScript basics", description: "A first programming language from scratch" } + ], + total: 3, + skip: 0, + limit: 30, +}) model Courses { courses: Course[]; total: uint16; diff --git a/typespec/postman/services/commentsService.tsp b/typespec/postman/services/commentsService.tsp index 6c3101c..5f1d011 100644 --- a/typespec/postman/services/commentsService.tsp +++ b/typespec/postman/services/commentsService.tsp @@ -1,3 +1,6 @@ +// Эти маршруты обслуживает приложение, а не мок prism, см. custom-server/src/resources.js. +// Спецификация описывает настоящее поведение: skip, limit и select применяются, +// а запрос несуществующей записи отвечает 404. import "@typespec/http"; import "@typespec/rest"; import "../models/comment.tsp"; @@ -23,17 +26,17 @@ interface CommentService { @path id: string, @query select?: string - ): Comment; + ): Comment | NotFoundResponse; @useAuth(BearerAuth) @post - op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewCommentDto): Comment; + op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewCommentDto): CreatedResponse & Comment; @useAuth(BearerAuth) @patch - op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditCommentDto): Comment; + op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditCommentDto): Comment | NotFoundResponse; @useAuth(BearerAuth) @delete - op delete(@path id: string): void; + op delete(@path id: string): NoContentResponse | NotFoundResponse; } diff --git a/typespec/postman/services/postsService.tsp b/typespec/postman/services/postsService.tsp index 4ef969e..766cefa 100644 --- a/typespec/postman/services/postsService.tsp +++ b/typespec/postman/services/postsService.tsp @@ -1,3 +1,6 @@ +// Эти маршруты обслуживает приложение, а не мок prism, см. custom-server/src/resources.js. +// Спецификация описывает настоящее поведение: skip, limit и select применяются, +// а запрос несуществующей записи отвечает 404. import "@typespec/http"; import "@typespec/rest"; import "../models/post.tsp"; @@ -22,19 +25,19 @@ interface PostService { op get( @path id: string, @query select?: string - ): Post; + ): Post | NotFoundResponse; @useAuth(BearerAuth) @post - op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewPostDto): Post; + op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewPostDto): CreatedResponse & Post; @useAuth(BearerAuth) @patch - op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditPostDto): Post; + op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditPostDto): Post | NotFoundResponse; @useAuth(BearerAuth) @delete - op delete(@path id: string): void; + op delete(@path id: string): NoContentResponse | NotFoundResponse; @route("/{postId}/comments") op getComments( diff --git a/typespec/postman/services/tasksService.tsp b/typespec/postman/services/tasksService.tsp index 87bbb0e..21110e7 100644 --- a/typespec/postman/services/tasksService.tsp +++ b/typespec/postman/services/tasksService.tsp @@ -1,3 +1,6 @@ +// Эти маршруты обслуживает приложение, а не мок prism, см. custom-server/src/resources.js. +// Спецификация описывает настоящее поведение: skip, limit и select применяются, +// а запрос несуществующей записи отвечает 404. import "@typespec/http"; import "@typespec/rest"; import "../models/task.tsp"; @@ -13,8 +16,8 @@ namespace AppService; interface TaskService { @get op list( - @query skip?: string, - @query limit?: string, + @query skip?: uint16 = 0, + @query limit?: uint16 = 30, @query select?: string[] ): Tasks; @@ -23,17 +26,17 @@ interface TaskService { op get( @path id: string, @query select?: string - ): Task; + ): Task | NotFoundResponse; @useAuth(BasicAuth) @post - op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewTaskDto): Task; + op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewTaskDto): CreatedResponse & Task; @useAuth(BasicAuth) @patch - op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditTaskDto): Task; + op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditTaskDto): Task | NotFoundResponse; @useAuth(BasicAuth) @delete - op delete(@path id: string): void; + op delete(@path id: string): NoContentResponse | NotFoundResponse; } diff --git a/typespec/postman/services/usersService.tsp b/typespec/postman/services/usersService.tsp index c2e23cd..a02323b 100644 --- a/typespec/postman/services/usersService.tsp +++ b/typespec/postman/services/usersService.tsp @@ -1,3 +1,6 @@ +// Эти маршруты обслуживает приложение, а не мок prism, см. custom-server/src/resources.js. +// Спецификация описывает настоящее поведение: skip, limit и select применяются, +// а запрос несуществующей записи отвечает 404. import "@typespec/http"; import "@typespec/rest"; import "../models/user.tsp"; @@ -16,8 +19,8 @@ namespace AppService; interface UserService { @get op list( - @query skip?: string, - @query limit?: string, + @query skip?: uint16 = 0, + @query limit?: uint16 = 30, @query select?: string[] ): Users; @@ -25,18 +28,18 @@ interface UserService { op get( @path id: string, @query select?: string - ): User; + ): User | NotFoundResponse; @post - op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewUserDto): User; + op create(@header contentType: "application/json" | "application/x-www-form-urlencoded", ...NewUserDto): CreatedResponse & User; @useAuth(BearerAuth) @patch - op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditUserDto): User; + op update(@header contentType: "application/json" | "application/x-www-form-urlencoded", @path id: string, ...EditUserDto): User | NotFoundResponse; @useAuth(BearerAuth) @delete - op delete(@path id: string): void; + op delete(@path id: string): NoContentResponse | NotFoundResponse; @route("/{authorId}/posts") op getPosts(