npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

socar-api-client

v0.1.3

Published

SoCar V4 API Client — unified data access layer for socar-data-platform and socar-manager-platform

Readme

socar-api-client

JSON:API + OpenAPI schema 运行时驱动 — 零手写 CRUD。

符合 JSON:API 规范的 OpenAPI schema 驱动客户端。schema 在哪里,filter 映射、URL 路由、 关系发现、权限判定就在哪里——不写胶水代码、不手写 service、不维护重复的 filterMapper。

import { ApiClient } from 'socar-api-client'

await ApiClient.init({ schema: '/path/to/schema.json', repository, guarder })

// 一个 resource 对应一个 JSON:API type,直接 CRUD
const sales = ApiClient.resource('sales')
const { data } = await sales.find({ filter: { time: '2024-01' } })

安装

pnpm add socar-api-client

模块总览

| 模块 | 导出 | 干什么 | 什么时候用 | |------|------|-------|-----------| | ApiClient | ApiClient | 静态外观。管理初始化、resource 工厂、权限查询、filter 格式 | 应用启动时 init,之后全局通过 ApiClient.resource\<T\>(type) 获取 resource | | ApiResource | ApiResource, JsonApiResponse | 泛型 CRUD。find / findById / create / update / delete + enrich 管道 | 拿到 resource 后直接用,不写 service | | SchemaRegistry | SchemaRegistry | 运行时加载 schema.json,自动推导 filter mapper、URL、relationships、protected 资源 | ApiClient.init 内部使用,也可独立用于 codegen 或检测 | | ServiceBase | ServiceBase | filter 处理引擎 + CRUD 委托基类。内部门面,不直接使用 | 旧迁移场景的基类,新代码通过 ApiResource | | Permission | Permission, PermissionError | 资源级权限判定。inject / can / isRoot,匹配规则:精确 > type.* > . | ApiClient 内部使用,也可独立使用 | | Enricher | Enricher, EnricherRegistry, enricherRegistry | 响应数据后处理管道。注册 enricher 后 ApiResource 在 { enrich: true } 时自动调用 | 需要给 API 响应加派生字段、格式化、裁剪时 | | SoCarDataLayer | SoCarDataLayer, Repository, supplier, scope, orbit, socarSource | 源码打包的 HTTP 客户端。axios + 多源管理 + token + scope 查询 + 新旧 filter 格式 | 初始化时创建 Repository,内部使用 | | Codegen(子路径) | generate, check | 根据 OpenAPI schema 生成 schema.json / types.d.ts / resources.ts | CLI 或 CI 中调用,产物给 SchemaRegistry 消费 |


架构

ApiClient          → 静态外观,管理生命周期
  ├─ SchemaRegistry → 加载 schema,推导 filter / URL / 关系 / 权限
  ├─ Permission     → 权限判定引擎
  └─ Repository     → SoCarDataLayer 封装

ApiResource<T>     → 泛型 CRUD,消费方唯一直接使用的接口
  ├─ ServiceBase   → filter 处理 + 方法委托
  └─ Enricher      → 响应后处理(可选)

SoCarDataLayer     → HTTP 客户端(axios),多源管理

Codegen             → 构建时工具,产出 schema 运行时文件

快速开始

import { ApiClient, SoCarDataLayer, Repository } from 'socar-api-client'

// 初始化
const repository = new Repository(SoCarDataLayer)
await ApiClient.init({
  schema: '/path/to/schema.json',       // URL 或本地路径
  repository,
  guarder: ({ method, type }) => ({ scope: ApiClient.can(type, method) }),
  permissions: ['*.*'],
  filterFormat: 'standard',             // 'standard'(新后端) | 'legacy'(旧后端)
})

// CRUD — 零手写 service、零 local filterMapper
const sales = ApiClient.resource('sales')

const { data: list } = await sales.find({
  filter: { time: '2024-01', group: ['bmw', 'audi'] },
  include: ['dealer'],
  page: { limit: 20 },
})

const { data: item } = await sales.findById('123')
const { data: created } = await sales.create({ name: '新车' })
const { data: updated } = await sales.update('123', { price: 110000 })
await sales.delete('123')

数据流

请求 → ApiResource → ServiceBase.filterHandler → SoCarDataLayer → 后端
         ↑                        ↑
   SchemaRegistry         filter 自动映射

API 参考

ApiClient

// 初始化(全部求值后一次性赋值,中间抛异常不污染旧状态)
ApiClient.init(config: {
  schema: string | object           // schema.json URL / 本地路径 / 已解析对象
  repository: unknown               // 平台 Repository 实例
  guarder: (params: { method: string; type: string }) => { scope?: boolean; url?: string }
  permissions?: string[]            // 初始权限列表,['*.*'] 放行全部
  filterFormat?: 'standard' | 'legacy'  // 默认 'standard' — 新后端结构化 filter 格式
}): Promise<void>

ApiClient.resource<T>(type: string): ApiResource<T>

// 权限
ApiClient.can(type: string, method?: string): boolean
ApiClient.injectPermissions(perms: string[]): void        // 运行时更新(角色切换 / token refresh)
ApiClient.getPermissionEntries(): string[]                 // 读取当前权限列表

ApiResource

interface ApiResource<T> {
  find<U>(options?: {
    filter?: Record<string, any>     // 原始 filter 参数 → SchemaRegistry 自动映射
    include?: string[]               // JSON:API include
    page?: { limit?: number; offset?: number }
    sort?: string
    fields?: string[]
    enrich?: boolean                 // 走 enricher 管道
  }): Promise<JsonApiResponse<U>>

  findById<U>(id: string, options?: {
    include?: string[]
    enrich?: boolean
  }): Promise<JsonApiResponse<U>>

  create<U>(data: Partial<T>, options?: {
    include?: string[]
  }): Promise<JsonApiResponse<U>>

  update<U>(id: string, data: Partial<T>): Promise<JsonApiResponse<U>>
  delete(id: string): Promise<void>
}

// 全部返回信封:
// { data: U, included?: RelatedResource[], meta?: Record<string, any>, links?: Record<string, string> }

SchemaRegistry

const registry = await SchemaRegistry.load('/path/to/schema.json')
// 也支持已解析对象
const registry = await SchemaRegistry.load(parsedObject)

registry.getFilterMapper('sales')
// → { time: { op: 'bt' }, group: { op: 'aeq', key: 'group.id' } }

registry.getResourceUrl('sales')              // → '/sales'
registry.getResourceUrlMap()                  // → { sales: '/sales', ... }
registry.getRelationships('sales')            // → { dealer: { type, kind }, ... }
registry.getProtectedResources()              // → Set<'sales', 'price', ...>

默认操作符推导(D10):

| valueType | 默认 op | 说明 | |-----------|---------|------| | string | eq | 等值 | | integer / number | eq | 等值 | | boolean | eq | 等值 | | datetime | bt | 范围 | | list | ct | 包含 |

如果默认 op 不在 OpenAPI 声明的 valid operators 中,自动 fallback 到第一个 valid op,并输出警告。

Permission

const perm = Permission.fromSchemaRegistry(schemaRegistry)
perm.inject(['sales.get', 'price.*'])       // 注入权限
perm.can('sales', 'get')                    // true / false
perm.isRoot                                 // 是否持有 *.*

匹配优先级精确匹配 > type.* > *.* > 未保护资源默认允许

FilterHandler (ServiceBase)

filter 参数全部通过 filterHandler 处理,自动映射字段名和操作符,输出结构化 filter[field][op]=value 格式。

service.filterHandler({ status: 0, name: 'test', time: undefined, group: null }, mapper)
// → { __socar__filter__: { status: { op: 'eq', value: 0 }, name: { op: 'ct', value: 'test' } } }
//    time(undefined) 和 group(null) 被跳过
  • 0 / false / '' 作为有效过滤值处理(不再被 if (val) 跳过)
  • null / undefined 跳过
  • 对象值 { op, value } 作为标准 filter 直接透传
  • 未在 mapper 中定义的字段在开发环境输出 console.warn

OR 过滤:传入 __socar_or_filter__ 键,自动合并为 AND 语义并输出 console.warn 提示(新后端不支持 OR)。

关系操作

await service.updateRelationship({ id: '1', type: 'user' }, { key: 'roles', data: [{ id: '2' }] })
await service.removeRelationship({ id: '1', type: 'user' }, { key: 'roles', data: [{ id: '2' }] })
await service.createRelationship({ id: '1', type: 'user' }, { key: 'roles', data: [{ id: '2' }] })

三个方法统一校验:

  • identity — 非空,含 idtype
  • relationshipData — 非 Array,key 为非空 string
  • 校验始终开启(不依赖 NODE_ENV

Enricher

abstract class Enricher<TInput, TOutput = TInput> {
  abstract enrich(input: TInput): TOutput          // 必须覆写
  enrichMany(inputs: TInput[]): TOutput[]           // 默认委托给 enrich
}

注册到全局 enricherRegistry,ApiResource{ enrich: true } 时自动调用:

class HelpEnricher extends Enricher<HelpAttrs, HelpDisplay> {
  enrich(input: HelpAttrs): HelpDisplay {
    return { ...input, excerpt: input.content.slice(0, 80) }
  }
}
enricherRegistry.register('help', new HelpEnricher())

const { data } = await helpApi.find({ enrich: true })

SoCarDataLayer

源码打包,零外部依赖。支持:

  • JSON:API HTTP 客户端(axios-based)
  • 多数据源管理(remote / memory / orbit)
  • 新旧后端 filter 格式切换(filterFormat
  • Token 管理
  • Scope 查询
  • 延迟初始化
import { SoCarDataLayer, supplier, scope, orbit, socarSource } from 'socar-api-client'

Codegen

位于独立子路径 socar-api-client/codegen,不包含在主入口中(避免 Node 依赖泄露到浏览器 bundle):

import { generate, check } from 'socar-api-client/codegen'

// 或通过 CLI
// npx socar-codegen ...
# 根据 OpenAPI schema 生成 3 个文件
npx socar-codegen --url https://data.dash.so.car/ --out ./src/__generated__

# 对比 committed 版本,确认 schema 未过期
npx socar-codegen --url https://data.dash.so.car/ --out ./src/__generated__ --check

# 冒烟测试
pnpm codegen:check

| 文件 | 用途 | 替代旧产物 | |------|------|-----------| | schema.json | 运行时 SchemaRegistry 消费 | filterMapper.ts / scope.json / ResourceUrlMap | | types.d.ts | 编译时类型接口 | openapi-types.ts | | resources.ts | ResourceType 枚举常量 | ResourceType(仅保留枚举) |


错误处理

import { PermissionError } from 'socar-api-client'

try {
  await sales.create(data)
} catch (err) {
  if (err instanceof PermissionError) {
    // statusCode = 403 (Forbidden) — 已认证但无权限
    // 使用 instanceof 判断,不要检查 statusCode
  }
}

JSON:API

本库严格遵循 JSON:API 规范,请求和响应均使用 application/vnd.api+json

响应信封

所有 CRUD 方法返回统一信封:

interface JsonApiResponse<T> {
  data: T                     // 单体或集合
  included?: RelatedResource[]  // include 查询的关联资源
  meta?: Record<string, any>    // 分页、聚合等元信息
  links?: Record<string, string> // 分页链接(self / next / prev / first / last)
}

请求体(create / update)

// 库内部自动包装为 JSON:API 格式,你传原始数据即可
sales.create({ name: '新车', price: 110000 })
// 实际发送: { data: { type: 'sales', attributes: { name: '新车', price: 110000 } } }

sales.update('123', { price: 120000 })
// 实际发送: { data: { type: 'sales', id: '123', attributes: { price: 120000 } } }

Resource Identifier

关系操作和权限判定使用统一标识格式:

type ResourceIdentifier = { id: string; type: string }

过滤参数

OpenAPI 声明式驱动,输出结构化 filter 参数:

原始:    { time: '2024-01', group: ['bmw', 'audi'] }
实际发送: filter[time][bt]=2024-01&filter[group.id][aeq]=bmw,audi
  • 新旧后端通过 filterFormat 切换('standard' | 'legacy'
  • 新后端(标准):结构化 filter[field][op]=value
  • 旧后端(legacy):Orbit 编码 filter=<encoded>

迁移

extends ServiceBase 的 service 和新 ApiResource 可以共存,逐模块迁移即可。

详见 docs/migration-guide.md


开发

pnpm install
pnpm build             # tsc 编译
pnpm typecheck         # tsc --noEmit
pnpm test              # vitest run(140+ 测试)
pnpm test:watch        # vitest 监听模式
pnpm codegen:check     # codegen 冒烟测试
pnpm clean             # 清空 dist

License

Proprietary