Skip to content

Commit 5fed91d

Browse files
committed
feat(query): public identifier()/unsafe, backtick escaping, docs and tests
- API: add sql.identifier() and sql.unsafe() to QueryClient and re-export from @ydbjs/query. - Security: identifier() now escapes backticks inside names (`` ` → ````) to prevent injection via identifiers. - Docs: - README.md: link to Russian README, section on identifiers and unsafe fragments, note that options apply only to single execute calls, add missing StatsMode imports in examples. - Add README.ru.md with comprehensive guide and examples. - SECURITY.md: examples migrated to sql/query(driver), add allow-list/validation patterns for dynamic identifiers. - package.json: update homepage to package page. - Tests: cover backtick escaping in identifier(), verify public re-exports of identifier/unsafe, minor formatting fixes. No breaking changes. Signed-off-by: Vladislav Polyakov <[email protected]>
1 parent bd3dfd8 commit 5fed91d

File tree

7 files changed

+353
-17
lines changed

7 files changed

+353
-17
lines changed

packages/query/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# @ydbjs/query
22

3+
Read this in Russian: [README.ru.md](README.ru.md)
4+
35
The `@ydbjs/query` package provides a high-level, type-safe client for executing YQL queries and managing transactions in YDB. It features a tagged template API, automatic parameter binding, transaction helpers, and deep integration with the YDB type system.
46

57
## Features
@@ -89,6 +91,7 @@ await sql.begin({ isolation: 'snapshotReadOnly', idempotent: true }, async (tx)
8991
### Advanced: Multiple Result Sets, Streaming, and Events
9092

9193
```ts
94+
import { StatsMode } from '@ydbjs/api/query'
9295
// Multiple result sets
9396
type Result = [[{ id: number }], [{ count: number }]]
9497
const [rows, [{ count }]] = await sql<Result>`SELECT id FROM users; SELECT COUNT(*) as count FROM users;`
@@ -116,13 +119,16 @@ try {
116119
### Query Options and Chaining
117120

118121
```ts
122+
import { StatsMode } from '@ydbjs/api/query'
119123
await sql`SELECT * FROM users`
120124
.isolation('onlineReadOnly', { allowInconsistentReads: true })
121125
.idempotent(true)
122126
.timeout(5000)
123127
.withStats(StatsMode.FULL)
124128
```
125129

130+
Note: isolation(), idempotent(), timeout(), and withStats() apply to single execute calls only; they are ignored inside transactions (sql.begin/sql.transaction).
131+
126132
### Value Conversion and Type Safety
127133

128134
All parameter values are converted using `@ydbjs/value`. See its documentation for details on supported types and conversion rules. You can pass native JS types, or use explicit YDB value classes for full control.
@@ -137,11 +143,34 @@ await sql`SELECT * FROM users WHERE meta = ${fromJs({ foo: 'bar' })}`
137143
You can enable and access query execution statistics:
138144

139145
```ts
146+
import { StatsMode } from '@ydbjs/api/query'
140147
const q = sql`SELECT * FROM users`.withStats(StatsMode.FULL)
141148
await q
142149
console.log(q.stats())
143150
```
144151

152+
## Identifiers and Unsafe Fragments
153+
154+
- Use identifiers for dynamic table/column names:
155+
156+
```ts
157+
// As a method on the client
158+
await sql`SELECT * FROM ${sql.identifier('users')}`
159+
160+
// Or import from the package if needed
161+
import { identifier } from '@ydbjs/query'
162+
await sql`SELECT * FROM ${identifier('users')}`
163+
```
164+
165+
- Use unsafe only for trusted SQL fragments (never with user input):
166+
167+
```ts
168+
import { unsafe } from '@ydbjs/query'
169+
await sql`SELECT * FROM users ${unsafe('ORDER BY created_at DESC')}`
170+
```
171+
172+
Security note: identifier() only quotes the name and escapes backticks. Do not pass untrusted input without validation/allow‑listing.
173+
145174
## Development
146175

147176
### Building the Package

packages/query/README.ru.md

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# @ydbjs/query
2+
3+
Читать на английском: [README.md](README.md)
4+
5+
`@ydbjs/query` — высокоуровневый, типобезопасный клиент для выполнения YQL‑запросов и управления транзакциями в YDB. Поддерживает теговый шаблонный синтаксис, автоматическое связывание параметров, хелперы транзакций и глубокую интеграцию с типовой системой YDB.
6+
7+
## Возможности
8+
9+
- Теговый шаблонный синтаксис для YQL
10+
- Типобезопасное связывание параметров (включая сложные/вложенные типы)
11+
- Транзакции с настройками изоляции и идемпотентности
12+
- Несколько результирующих наборов и стриминг
13+
- Статистика выполнения и диагностика
14+
- Полные типы TypeScript
15+
16+
## Установка
17+
18+
```sh
19+
npm install @ydbjs/core@alpha @ydbjs/query@alpha
20+
```
21+
22+
## Как это работает
23+
24+
- **Query‑клиент**: создайте клиент `query(driver)`. Он возвращает теговую функцию для YQL и хелперы транзакций.
25+
- **Сессии и транзакции**: управление жизненным циклом выполняется автоматически. Можно запускать одиночные запросы или объединять несколько запросов в транзакцию через `begin`/`transaction`.
26+
- **Параметры**: параметры передаются через интерполяцию (`${}`) в шаблоне. Поддерживаются нативные типы JS, классы значений YDB и массивы/объекты. Для именованных параметров используйте `.parameter()`/`.param()`.
27+
- **Безопасность типов**: все значения конвертируются с помощью `@ydbjs/value`. Сложные и вложенные типы обрабатываются автоматически.
28+
- **Результаты**: YDB может возвращать несколько наборов результатов за один запрос.
29+
- **Статистика**: используйте `.withStats()` или `.stats()` для доступа к статистике выполнения.
30+
31+
## Использование
32+
33+
### Быстрый старт
34+
35+
```ts
36+
import { Driver } from '@ydbjs/core'
37+
import { query } from '@ydbjs/query'
38+
39+
const driver = new Driver('grpc://localhost:2136/local')
40+
await driver.ready()
41+
42+
const sql = query(driver)
43+
const resultSets = await sql`SELECT 1 + 1 AS sum`
44+
console.log(resultSets) // [ [ { sum: 2 } ] ]
45+
```
46+
47+
### Параметризованные запросы
48+
49+
```ts
50+
const userId = 42n
51+
const userName = 'Alice'
52+
await sql`
53+
SELECT * FROM users
54+
WHERE id = ${userId} AND name = ${userName}
55+
`
56+
```
57+
58+
#### Именованные параметры и кастомные типы
59+
60+
```ts
61+
import { Uint64 } from '@ydbjs/value/primitive'
62+
const id = new Uint64(123n)
63+
await sql`SELECT * FROM users WHERE id = $id`.parameter('id', id)
64+
```
65+
66+
#### Массивы, структуры и табличные параметры
67+
68+
```ts
69+
const users = [
70+
{ id: 1, name: 'Alice' },
71+
{ id: 2, name: 'Bob' },
72+
]
73+
await sql`INSERT INTO users SELECT * FROM AS_TABLE(${users})`
74+
```
75+
76+
### Транзакции
77+
78+
```ts
79+
// Serializable read-write (по умолчанию)
80+
const result = await sql.begin(async (tx) => {
81+
await tx`UPDATE users SET active = false WHERE last_login < CurrentUtcTimestamp() - Interval('P1Y')`
82+
return await tx`SELECT * FROM users WHERE active = false`
83+
})
84+
85+
// С изоляцией и идемпотентностью
86+
await sql.begin({ isolation: 'snapshotReadOnly', idempotent: true }, async (tx) => {
87+
return await tx`SELECT COUNT(*) FROM users`
88+
})
89+
```
90+
91+
### Продвинутое: несколько наборов результатов, стриминг и события
92+
93+
```ts
94+
import { StatsMode } from '@ydbjs/api/query'
95+
// Несколько наборов результатов
96+
type Result = [[{ id: number }], [{ count: number }]]
97+
const [rows, [{ count }]] = await sql<Result>`SELECT id FROM users; SELECT COUNT(*) as count FROM users;`
98+
99+
// Подписка на статистику и ретраи
100+
const q = sql`SELECT * FROM users`.withStats(StatsMode.FULL)
101+
q.on('stats', (stats) => console.log('Query stats:', stats))
102+
q.on('retry', (ctx) => console.log('Retrying:', ctx))
103+
await q
104+
```
105+
106+
### Обработка ошибок
107+
108+
```ts
109+
import { YDBError } from '@ydbjs/error'
110+
try {
111+
await sql`SELECT * FROM non_existent_table`
112+
} catch (e) {
113+
if (e instanceof YDBError) {
114+
console.error('YDB Error:', e.message)
115+
}
116+
}
117+
```
118+
119+
### Опции запроса и чейнинг
120+
121+
```ts
122+
import { StatsMode } from '@ydbjs/api/query'
123+
await sql`SELECT * FROM users`
124+
.isolation('onlineReadOnly', { allowInconsistentReads: true })
125+
.idempotent(true)
126+
.timeout(5000)
127+
.withStats(StatsMode.FULL)
128+
```
129+
130+
Внимание: эти опции действуют только для одиночных запросов (один вызов execute). Внутри транзакций (sql.begin/sql.transaction) они игнорируются.
131+
132+
### Конвертация значений и безопасность типов
133+
134+
Все значения конвертируются с помощью `@ydbjs/value`. См. документацию `@ydbjs/value` по типам и правилам конвертации. Можно передавать нативные типы JS или использовать классы YDB для полного контроля.
135+
136+
```ts
137+
import { fromJs } from '@ydbjs/value'
138+
await sql`SELECT * FROM users WHERE meta = ${fromJs({ foo: 'bar' })}`
139+
```
140+
141+
## Статистика запросов
142+
143+
```ts
144+
import { StatsMode } from '@ydbjs/api/query'
145+
const q = sql`SELECT * FROM users`.withStats(StatsMode.FULL)
146+
await q
147+
console.log(q.stats())
148+
```
149+
150+
## Идентификаторы и небезопасные фрагменты
151+
152+
- Динамические имена таблиц/колонок — используйте идентификаторы:
153+
154+
```ts
155+
// Метод клиента
156+
await sql`SELECT * FROM ${sql.identifier('users')}`
157+
158+
// Или импорт из пакета
159+
import { identifier } from '@ydbjs/query'
160+
await sql`SELECT * FROM ${identifier('users')}`
161+
```
162+
163+
- Небезопасные фрагменты — только для доверенных сценариев (не с данными пользователя):
164+
165+
```ts
166+
import { unsafe } from '@ydbjs/query'
167+
await sql`SELECT * FROM users ${unsafe('ORDER BY created_at DESC')}`
168+
```
169+
170+
Заметка по безопасности: identifier() лишь экранирует обратные кавычки и оборачивает имя в обратные кавычки. Не передавайте туда непроверенный ввод — используйте валидацию/allow‑list.
171+
172+
## Разработка
173+
174+
### Сборка
175+
176+
```sh
177+
npm run build
178+
```
179+
180+
### Тесты
181+
182+
```sh
183+
npm test
184+
```
185+
186+
## Настройки для AI ассистентов
187+
188+
Этот пакет содержит примеры конфигураций для AI‑ассистентов в каталоге `ai-instructions/`, чтобы генерировать безопасный YQL‑код.
189+
190+
Быстрый старт:
191+
192+
```bash
193+
# Для Cursor AI
194+
cp node_modules/@ydbjs/query/ai-instructions/.cursorrules.example .cursorrules
195+
196+
# Для GitHub Copilot
197+
cp node_modules/@ydbjs/query/ai-instructions/.copilot-instructions.example.md .copilot-instructions.md
198+
199+
# Для прочих ассистентов
200+
cp node_modules/@ydbjs/query/ai-instructions/.instructions.example.md .instructions.md
201+
# ИЛИ
202+
cp node_modules/@ydbjs/query/ai-instructions/.ai-instructions.example.md .ai-instructions.md
203+
```
204+
205+
См. `SECURITY.md` для полного руководства по безопасности.
206+
207+
## Лицензия
208+
209+
Проект распространяется по лицензии [Apache 2.0](../../LICENSE).
210+
211+
## Ссылки
212+
213+
- Документация YDB: https://ydb.tech
214+
- Репозиторий: https://github.com/ydb-platform/ydb-js-sdk
215+
- Issues: https://github.com/ydb-platform/ydb-js-sdk/issues

0 commit comments

Comments
 (0)