@ verystack/ api (英语)
PostgREST相容的HTTP处理器并打入Drizzle ORM取取客户端.
安装
pnpm add @everystack/api drizzle-orm
条目点
| 导入 | 说明 |
|---|---|
@everystack/api/handler | 服务器侧 PostgREST 处理器 |
@everystack/api/client | 客户端键入取取客户端 |
处理器
创建网络标准 (Request) => Promise<Response> 处理器来自 Drizzle 数据库实例和计划。 与Expo Router API线路, Cloudflare Workers, Bun, Deno 或任何支持请求/响应接口的平台合作.
import { createHandler } from '@everystack/api/handler';
import { db } from './db';
import * as schema from './db/schema';
const handler = createHandler(db, schema);
挂载为 Expo 路由器 API 路由
// app/api/[...path]+api.ts
import { createHandler } from '@everystack/api/handler';
import { db } from '../../db';
import * as schema from '../../db/schema';
const handler = createHandler(db, schema, { basePath: '/api' });
export function GET(request: Request) { return handler(request); }
export function POST(request: Request) { return handler(request); }
export function PATCH(request: Request) { return handler(request); }
export function DELETE(request: Request) { return handler(request); }
处理器选项
createHandler(db, schema, {
basePath: '/api',
auth: {
verifyToken: async (token) => verifyJWT(token),
publicRoutes: ['GET'],
publicRpc: ['health', 'search'],
onAuthenticated: (user) => {},
// Role-based access control
roles: {
posts: { read: 'public', write: 'authenticated' },
admin_logs: { read: 'admin', write: 'admin' },
'*': { read: 'authenticated', write: 'authenticated' },
},
roleHierarchy: ['public', 'authenticated', 'moderator', 'admin'],
roleField: 'role',
// Client credentials (two-tier auth)
verifyClient: async (clientId, clientSecret) => lookupClient(clientId, clientSecret),
clientHeaders: { id: 'X-Client-Id', secret: 'X-Client-Secret' },
onClientAuthenticated: (client) => {},
},
rpc: {
async timeline(body, user, client) {
return db.query.posts.findMany({ limit: 20 });
},
},
relations: {
posts: {
author: { table: 'users', from: 'authorId', to: 'id' },
comments: { table: 'comments', from: 'id', to: 'postId', many: true },
},
},
// lifecycle: true tables, the one soft delete. deriveHandlerConfig(models) emits this list;
// do not write it by hand. Restore is POST {basePath}/{table}/restore, client .restore(id).
lifecycle: ['posts', 'comments'],
pgSettings: (user, client) => ({
'request.jwt.claims': JSON.stringify(user || { role: 'anon' }),
'app.user_id': String(user?.sub || ''),
}),
hooks: {
posts: {
beforeCreate: async (body, user, client) => ({ ...body, authorId: user?.sub }),
afterCreate: async (row, user, client) => { /* notify */ },
beforeUpdate: async (body, user, client) => body,
afterUpdate: async (rows, user, client) => {},
// All models; veto from '@everystack/api/handler' rolls back the request:
beforeDelete: (row, ctx) => (ctx.user ? undefined : veto('sign in to delete')), // in the transaction; veto -> 409
afterDelete: async (rows, ctx) => {}, // after commit, once per request; ctx.batch is null
afterRestore: async (rows, ctx) => {}, // after commit, ctx.batch and ctx.counts
},
},
naming: 'snake_case', // or 'camelCase' (default)
});
行级安全( pgSettings)
何时 pgSettings 提供,每个 CRUD 查询都被包裹在 PostgreSQL 交易中 set_config() 打电话 这将注入 RLS 政策可以通过 current_setting()。这些值是交易的LOCAL——它们从不通过集合连接泄露。 未提供时, 查询会直接执行而不进行包装 .
角色接入控制
何时 auth.roles 配置,每个表格路由对照HTTP方法所需的角色检查用户的角色. 决议: roles[tableName] → roles['*'] → null (倒计时) publicRoutes) (中文(简体) ). 特殊角色: 'public' (不需要任何标志), 'authenticated' (任何有效的JWT). 出错代码:401 (无/无效令牌),403(作用不足).
拒绝代码
认证拒绝保留其现有的 HTTP 状态 messageJSON机构增加了一个 code数字 :
| 代码 | 含义 |
|---|---|
token_refused | 出示的出入证被拒绝. |
refresh_refused | 拒绝更新令牌,包括重新使用或无法使用. |
auth_required | 没有提交所需的访问令牌. |
client_refused | 客户证书缺失或被拒绝. |
拒绝确认和密码的代号没有拒绝代码. 错误的密码,账户暂停,验证错误,以及服务中断没有拒绝代码. 没有公认代码的401不能表示百事可拒的会话.
这个 code 键还包含其他错误值,包括数据库错误上的 PostgreSQL SQLSTATEs 和 vetoed 在钩上否决。 客户端必须精确匹配四个拒绝值中的一个, 绝不只是检查是否 code 已存在.
自定义认证服务器可以使用相同的响应帮助 :
import { authRefusal, AUTH_REFUSAL_CODES, type AuthRefusalCode } from '@everystack/api/handler';
return authRefusal('token_refused', 'Invalid or expired token'); // status defaults to 401
authRefusal(code, message, status?) 准确返回 { message, code } 与 Content-Type: application/json它没有运行时间依赖. AuthRefusalCode 是四个条目的结合 AUTH_REFUSAL_CODES。 。 。 .
命名公约
这个 naming 选项控制密钥在 API 边界的转换 :
'snake_case':接受蛇 病例入出,返回蛇 病例出出出出出. POST/PATCH 天体映射出蛇 病例 为德瑞兹尔而取出骆驼Case.'camelCase'或省略:将 Drizzle 属性名称作为-is.
列分辨率为双向 : ?author_id=eq.5 决定: authorId 自动,即使没有 naming 选项.
查询协议
处理器执行 PostgREST 查询协议 :
过滤器 (单位:千美元)?column=operator.value:
| 运算符 | 说明 | 示例 |
|---|---|---|
eq | 等数 | ?status=eq.published |
neq | 不等于一个 | ?role=neq.admin |
gt 编号 : gte | 超过 | ?age=gt.18 |
lt 编号 : lte | 少于 | ?price=lt.100 |
like | 模式匹配 | ?name=like.*smith* |
ilike | 不区分大小写模式 | ?email=ilike.*@gmail.com |
in | 设定 | ?id=in.(1,2,3) |
is | 无效检查 | ?deletedAt=is.null |
否认 (单位:千美元)not. 前缀 : ?status=not.eq.draft, (中文(简体) ), ?tags=not.in.(a,b), (中文(简体) ), ?deletedAt=not.is.null
逻辑组数字 :
?or=(status.eq.draft,status.eq.pending)- 其他资源条件?and=(price.gte.10,price.lte.100)- 明确和分组- 与正则过滤器组合 :
?category=eq.electronics&or=(status.eq.draft,status.eq.pending)
合计数 (邮政 column.function() 语法 :
?select=count()——数起所有行?select=platform,total:amount.sum()- 以别名组成的集团- 获得支持:
count(), (中文(简体) ),sum(), (中文(简体) ),avg(), (中文(简体) ),min(), (中文(简体) ),max()
JSON 路径过滤数字 : ?metadata->>theme=eq.dark, (中文(简体) ), ?data->settings->>mode=eq.compact
列选择数字 : ?select=id,title,author(name,email)
嵌入关系数字 : ?select=*,author(*),comments(body)
订购数字 : ?order=createdAt.desc
倾斜数字 : ?limit=10&offset=20
精确计数: 发送 Prefer: count=exact 要获取的页眉 Content-Range: 0-9/42
变异数字 :
POST /table- 插入(返回创建的行,201)PATCH /table?id=eq.1— 更新匹配行 (200)DELETE /table?id=eq.1— 删除匹配行(需要过滤器, 200)
难民保护委员会数字 : POST /rpc/function_name 与JSON体
客户端
纯取零反应依赖的客户端 。 在任何 JavaScript 运行时间工作 .
import { createClient } from '@everystack/api/client';
import { session } from '@everystack/auth/api';
const api = createClient({
baseUrl: '/api',
session: session.forClient(),
headers: { 'X-Custom': 'value' },
});
session 优先于被消耗的 getToken, (中文(简体) ), onTokenExpired 财务报告和审定财务报表 onAuthFailure 召回电话.
使用 createSession 从 @everystack/auth/session 用于自定义存储或认证.
API 软件包接受结构 ClientSession 接口,不导入认证软件包.
查询数据
// List with filters
const { data, error } = await api
.from('posts')
.eq('status', 'published')
.order('createdAt', 'desc')
.limit(10)
.execute();
// Single record
const { data: post } = await api
.from('posts')
.eq('id', '123')
.single();
// With exact count
const { data: posts, total } = await api
.from('posts')
.limit(20)
.executeWithCount();
// Column selection + relation embedding
const { data } = await api
.from('posts')
.select('id,title,author(name)')
.execute();
// Negation
const { data } = await api
.from('posts')
.notEq('status', 'draft')
.notIn('category', ['spam', 'archived'])
.execute();
// Logical groups
const { data } = await api
.from('posts')
.eq('active', 'true')
.or('status.eq.draft,status.eq.pending')
.execute();
// Aggregates
const { data } = await api
.from('transactions')
.select('platform,total:amount.sum()')
.execute();
变异
// Insert
const { data } = await api.from('posts').insert({
title: 'Hello',
content: 'World',
});
// Update
const { data } = await api
.from('posts')
.eq('id', '123')
.update({ title: 'Updated' });
// Delete
const { data } = await api
.from('posts')
.eq('id', '123')
.remove();
过滤方法
所有过滤方法都返回构建器进行链化:
// Standard operators
.eq(column, value) // Equals
.neq(column, value) // Not equals
.gt(column, value) // Greater than
.gte(column, value) // Greater or equal
.lt(column, value) // Less than
.lte(column, value) // Less or equal
.like(column, value) // LIKE pattern
.ilike(column, value) // Case-insensitive LIKE
.is(column, value) // IS NULL check
.in(column, values[]) // IN array
// Negation operators
.notEq(column, value) // NOT equal
.notNeq(column, value) // NOT not-equal
.notGt(column, value) // NOT greater than
.notGte(column, value) // NOT greater or equal
.notLt(column, value) // NOT less than
.notLte(column, value) // NOT less or equal
.notLike(column, value) // NOT LIKE
.notIlike(column, value)// NOT ILIKE
.notIs(column, value) // IS NOT NULL
.notIn(column, values[])// NOT IN array
// Logical groups
.or(expression) // OR group: .or('status.eq.draft,status.eq.pending')
.and(expression) // AND group: .and('price.gte.10,price.lte.100')
调试刷新
会议于401年更新一次 token_refused,然后客户端重复一次.
同步请求刷新 。 拒绝刷新或重试 token_refused 结束会话.
auth_required, (中文(简体) ), client_refused 外国401从不启动刷新或签署.
一个失败的刷新保存会话, 返回有代码的状态 503 refresh_unavailable。 。 。 .
session.forClient({ whenRefreshFails: 'anonymous' }) 换句话说,没有无记名标志就再试一次.
签名 出局总是会赢得晚点喝的.
legacyRefusals 默认为 true: 准确的旧的百事可答留言仍然有效 code 缺席.
准备吧 false 在部署发送代码的服务器后。 此选项在下次断开时消失 .
见 客户指南 财务报告和审定财务报表 届会指南。 。 。 .
导出类型
import type {
AuthConfig, RoleConfig, RelationConfig, SoftDeleteConfig,
MutationEvent, HookConfig, HandlerOptions,
} from '@everystack/api/handler';
import type {
ClientOptions, ClientError, ClientResponse,
CountResponse, QueryBuilder, Client,
} from '@everystack/api/client';
同伴依赖关系
| 软件包 | 版本 |
|---|---|
drizzle-orm | >=0.35.0 |
部分 无所事事 ——AWS上的"博览会"应用程序自发托管的应用程序堆栈.
许可证
仅限AGPL-3.0 • 可扩展技术公司.
向不能或不希望获得商业许可证的组织发放商业许可证 遵守AGPL-3.0条款。 关于商业许可证,联系 许可证发放@可扩展性.技术。 。 。 .